上篇我们介绍了Free类型可以作为一种嵌入式编程语言DSL在函数式编程中对某种特定功能需求进行描述。一个完整的应用可能会涉及多样的关联功能,但如果我们为每个应用都设计一套DSL的话,那么在我们的函数式编程中将会不断重复的功能相似的DSL。我们应该秉承函数式编程的核心思想:函数组合(compositionality)来实现DSL的组合:把DSL拆解成最基础语句ADT,然后用这些ADT来组合成适合应用功能要求的完整DSL。我们还是使用上篇那个Interact DSL,这次再增加一个Login功能:
1 package demo.app 2 import cats.free.{Free,Inject} 3 object FreeModules { 4 object ADTs { 5 sealed trait Interact[+A] 6 object Interact { 7 case class Ask(prompt: String) extends Interact[String] 8 case class Tell(msg: String) extends Interact[Unit] 9 type FreeInteract[A] = Free[Interact,A] 10 def ask(prompt: String): FreeInteract[String] = Free.liftF(Ask(prompt)) 11 def tell(msg: String): FreeInteract[Unit] = Free.liftF(Tell(msg)) 12 } 13
14 sealed trait Login[+A] 15 object Login { 16 type FreeLogin[A] = Free[Login,A] 17 case class Authenticate(user: String, pswd: String) extends Login[Boolean] 18 def authenticate(user: String, pswd: String): FreeLogin[Boolean] =
19 Free.liftF(Authenticate(user,pswd)) 20 } 21
22 } 23
24 }
上面我们增加了个Login类。我们先来进行DSL编程:
1 object DSLs { 2 import ADTs._ 3 import Interact._ 4 import Login._ 5 val interactDSL: FreeInteract[Unit] = for { 6 first <- ask("What's your first name?") 7 last <- ask("What's your last name?") 8 _ <- tell(s"Hello, $first $last!") 9 } yield() 10
11 val loginDSL: FreeLogin[Boolean] = for { 12 login <- authenticate("Tiger","123") 13 } yield login 14 }
很明显,用一种DSL编程是无法满足Login功能需要的。我们需要像下面这样的DSL:
1 val interactLoginDSL: Free[???,Boolean] = for { 2 uid <- ask("Enter your User ID:") 3 psw <- ask("Enter your Password:") 4 aut <- authenticate(uid,pwd) 5 } yield aut
不过上面的???应该是什么呢?它应该是Interact和Login的集合。cats提供了Coproduct,它是一个树形数据结构:
/** `F` on the left and `G` on the right of [[scala.util.Either]]. * * @param run The underlying [[scala.util.Either]]. */ final case class Coproduct[F[_], G[_], A](run: Either[F[A], G[A]]) {...}
Coproduct 的每一个节点(Either[F[A],G[A]])都是一个ADT,F[A]或者G[A]。我们可以用多层递归Coproduce结构来构建一个多语法的树形结构,如:
1 type H[A] = Coproduct[F,G,A] 2 type I[A] = Coproduct[H,X,A] 3 type J[A] = Coproduct[J,Y,A] //ADT(F,G,X,Y)
用Coproduct的树形结构可以容纳多种DSL的ADT。在上面的例子里我们需要一个组合的语法InteractLogin:
1 type InteractLogin[A] = Coproduct[Interact,Login,A]
cats提供了Inject类来构建Coproduct:
sealed abstract class Inject[F[_], G[_]] { def inj[A](fa: F[A]): G[A] def prj[A](ga: G[A]): Option[F[A]] } private[free] sealed abstract class InjectInstances { implicit def catsFreeReflexiveInjectInstance[F[_]]: Inject[F, F] =
new Inject[F, F] { def inj[A](fa: F[A]): F[A] = fa def prj[A](ga: F[A]): Option[F[A]] = Some(ga) } implicit def catsFreeLeftInjectInstance[F[_], G[_]]: Inject[F, Coproduct[F, G, ?]] =
new Inject[F, Coproduct[F, G, ?]] { def inj[A](fa: F[A]): Coproduct[F, G, A] = Coproduct.leftc(fa) def prj[A](ga: Coproduct[F, G, A]): Option[F[A]] = ga.run.fold(Some(_), _ => None) } implicit def catsFreeRightInjectInstance[F[_], G[_], H[_]](implicit I: Inject[F, G]): Inject[F, Coproduct[H, G, ?]] =
new Inject[F, Coproduct[H, G, ?]] { def inj[A](fa: F[A]): Coproduct[H, G, A] = Coproduct.rightc(I.inj(fa)) def prj[A](ga: Coproduct[H, G, A]): Option[F[A]] = ga.run.fold(_ => None, I.prj) } }
inj[A](fa: F[A]):G[A]代表将F[A]注入更大的语法集G[A]。cats提供了三种实现了ink函数的Inject隐式实例:
1、catsFreeReflexiveInjectInstance:Inject[F,F]:对单一语法,