在上一节我们讨论了通过Coproduct来实现DSL组合:用一些功能简单的基础DSL组合成符合大型多复杂功能应用的DSL。但是我们发现:cats在处理多层递归Coproduct结构时会出现编译问题。再就是Free编程是一个繁复的工作,容易出错,造成编程效率的低下。由于Free编程目前是函数式编程的主要方式(我个人认为),我们必须克服Free编程的效率问题。通过尝试,发现freeK可以作为一个很好的Free编程工具。freeK是个开源的泛函组件库,我们会在这次讨论里用freeK来完成上次讨论中以失败暂停的多层Coproduct Free程序。我们先试试Interact和Login两个混合DSL例子:
1 object ADTs { 2 sealed trait Interact[+A] 3 object Interact { 4 case class Ask(prompt: String) extends Interact[String] 5 case class Tell(msg: String) extends Interact[Unit] 6 } 7 sealed trait Login[+A] 8 object Login { 9 case class Authenticate(uid: String, pwd: String) extends Login[Boolean] 10 } 11 } 12 object DSLs { 13 import ADTs._ 14 import Interact._ 15 import Login._ 16 type PRG = Interact :|: Login :|: NilDSL 17 val PRG = DSL.Make[PRG] 18
19 val authenticDSL: Free[PRG.Cop, Boolean] =
20 for { 21 uid <- Ask("Enter your user id:").freek[PRG] 22 pwd <- Ask("Enter password:").freek[PRG] 23 auth <- Authenticate(uid,pwd).freek[PRG] 24 } yield auth 25 }
从ADT到DSL设计,用freeK使代码简单了很多。我们不需要再对ADT进行Inject和Free.liftF升格了,但必须在没条语句后附加.freek[PRG]。本来可以通过隐式转换来避免这样的重复代码,但scalac会在编译时产生一些怪异现象。这个PRG就是freeK的Coproduct结构管理方法,PRG.Cop就是当前的Coproduct。freeK是用:|:符号来连接DSL的,替代了我们之前繁复的Inject操作。
功能实现方面有什么变化吗?
1 object IMPLs { 2 import ADTs._ 3 import Interact._ 4 import Login._ 5 val idInteract = new (Interact ~> Id) { 6 def apply[A](ia: Interact[A]): Id[A] = ia match { 7 case Ask(p) => {println(p); scala.io.StdIn.readLine} 8 case Tell(m) => println(m) 9 } 10 } 11 val idLogin = new (Login ~> Id) { 12 def apply[A](la: Login[A]): Id[A] = la match { 13 case Authenticate(u,p) => (u,p) match { 14 case ("Tiger","123") => true
15 case _ => false
16 } 17 } 18 } 19 val interactLogin = idInteract :&: idLogin 20 }
这部分没有什么变化。freeK用:&:符号替换了or操作符。
那我们又该如何运行用freeK编制的程序呢?
1 object freeKDemo extends App { 2 import FreeKModules._ 3 import DSLs._ 4 import IMPLs._ 5 val r0 = authenticDSL.foldMap(interactLogin.nat) 6 val r = authenticDSL.interpret(interactLogin) 7 println(r0) 8 println(r) 9 }
interactLogin.nat就是以前的G[A]~>Id,所以我们依然可以用cats提供的foldMap来运算。不过freeK提供了更先进的interpret函数。它的特点是不要求Coproduct结构的构建顺序,我们无须再特别注意用inject构建Coproduct时的先后顺序了。也就是说:|:和:&:符号的左右元素可以不分,这将大大提高编程效率。
我们还是按上次的功能设计用Reader来进行用户密码验证功能的依赖注入。依赖界面定义如下:
1 object Dependencies { 2 trait PasswordControl { 3 val mapPasswords: Map[String,String] 4 def matchUserPassword(uid: String, pwd: String): Boolean 5 } 6 }
我们需要把Interact和Login都对应到Reader:
1 import Dependencies._ 2 type ReaderContext[A] = Reader[PasswordControl,A] 3 object readerInteract extends (Interact ~> ReaderContext) { 4 def apply[A](ia: Interact[A]): ReaderContext[A] = ia match { 5 case Ask(p) => Reader {pc => {println(p); scala.io.StdIn.readLine}} 6 case Tell(m) => Reader {_ => println(m)} 7 } 8 } 9 object readerLogin extends (Login ~> ReaderContext) { 10 def apply[A](la: Login[A]): ReaderContext[A] = la match { 11 case Authenticate(u,p) => Reader {pc => pc.matchUserPassword(u,p)} 12 } 13 } 14 val userInteractLogin = readerLogin :&: readerInteract
注意在上面我故意调换了:&:符号两边对象来证明interpret函数是不依赖Coproduct顺序的。
运算时我们需要构建一个测试的PasswordControl实例,然后把它传入Reader.run函数:
1 object freeKDemo extends App { 2 import FreeKModules._ 3 import DSLs._ 4 import IMPLs._ 5 // val r0 = authenticDSL.foldMap(