在上一节我们讨论了通过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(interactLogin.nat) 6 // val r = authenticDSL.interpret(interactLogin) 7 import Dependencies._ 8 object UserPasswords extends PasswordControl { 9 override val mapPasswords: Map[String, String] = Map( 10 "Tiger" -> "123", 11 "John" -> "456" 12 ) 13 override def matchUserPassword(uid: String, pwd: String): Boolean = 14 mapPasswords.getOrElse(uid,pwd+"!") == pwd 15 } 16 17 interactLoginDSL.interpret(userInteractLogin).run(UserPasswords) 18 }
测试运行正常。现在我们要尝试三个独立DSL的组合了。先增加一个用户权限验证DSL:
1 sealed trait Auth[+A] 2 object Auth { 3 case class Authorize(uid: String) extends Auth[Boolean] 4 }
假如这个用户权限验证也是通过依赖注入的,我们先调整一下依赖界面:
1 object Dependencies { 2 trait PasswordControl { 3 val mapPasswords: Map[String,String] 4 def matchUserPassword(uid: String, pswd: String): Boolean 5 } 6 trait AccessControl { 7 val mapAccesses: Map[String, Boolean] 8 def grandAccess(uid: String): Boolean 9 } 10 trait Authenticator extends PasswordControl with AccessControl 11 }
我们用Authenticator来代表包括PasswordControl,AccessControl的所有外部依赖。这样我们就需要把Reader的传入对象改变成Authenticator:
1 import Dependencies._ 2 type ReaderContext[A] = Reader[Authenticator,A]
首先我们把增加的Auth语法与前两个语法构成的Coproduct再集合,然后进行集合三种语法的DSL编程:
1 import Auth._ 2 type PRG3 = Auth :|: PRG //Interact :|: Login :|: NilDSL 3 val PRG3 = DSL.Make[PRG3] 4 val authorizeDSL: Free[PRG3.Cop, Unit] = 5 for { 6 uid <- Ask("Enter your User ID:").freek[PRG3] 7 pwd <- Ask("Enter your Password:").freek[PRG3] 8 auth <- Authenticate(uid,pwd).freek[PRG3] 9 perm <- if (auth) Authorize(uid).freek[PRG3] 10 else Free.pure[PRG3.Cop,Boolean](false) 11 _ <- if (perm) Tell(s"Hello $uid, access granted!").freek[PRG3] 12 else Tell(s"Sorry $uid, access denied!").freek[PRG3] 13 } yield()
这个程序的功能具体实现方式如下:
1 val readerAuth = new (Auth ~> ReaderContext) { 2 def apply[A](aa: Auth[A]): ReaderContext[A] = aa match { 3 case Authorize(u) => Reader {ac => ac.grandAccess(u)} 4 } 5 } 6 val userAuth = readerAuth :&: userInteractLogin
下面是测试数据制作以及运算:
1 import Dependencies._ 2 object AuthControl extends Authenticator { 3 override val mapPasswords = Map( 4 "Tiger" -> "1234", 5 "John" -> "0000" 6 ) 7 override def matchUserPassword(uid: String, pswd: String) = 8 mapPasswords.getOrElse(uid, pswd+"!") == pswd 9 10 override val mapAccesses = Map ( 11 "Tiger" -> true, 12 "John" -> false 13 ) 14 override def grandAccess(uid: String) = 15 mapAccesses.getOrElse(uid, false) 16 } 17 18 // interactLoginDSL.interpret(userInteractLogin).run(AuthControl) 19 authorizeDSL.interpret(userAuth).run(AuthControl)
测试运行结果:
1 Enter your User ID: 2 Tiger 3 Enter your Password: 4 1234 5 Hello Tiger, access granted! 6 7 Process finished with exit code 0 8 ... 9 Enter your User ID: 10 John 11 Enter your Password: 12 0000 13 Sorry John, access denied! 14 15 Process finished with exit code 0
结果正是我们所预期的。在这次示范中我没费什么功夫就顺利的完成了一个三种语法DSL的编程示范。这说明freeK确实是个满意的Free编程工具。这次讨论的示范代码如下:
1 import cats.free.Free 2 import cats.{Id, ~>} 3 import cats.data.Reader 4 import demo.app.FreeKModules.ADTs.Auth.Authorize 5 import freek._ 6 object FreeKModules { 7 object ADTs { 8 sealed trait Interact[+A] 9 object Interact { 10 case class Ask(prompt: String) extends Interact[String] 11 case class Tell(msg: String) extends Interact[Unit] 12 } 13 sealed trait Login[+A] 14 object Login { 15 case class Authenticate(uid: String, pwd: String) extends Login[Boolean] 16 } 17 sealed trait Auth[+A] 18 object Auth { 19 case class Authorize(uid: String) extends Auth[Boolean] 20 } 21 } 22 object DSLs { 23 import ADTs._ 24 import Interact._ 25 import Login._ 26 type PRG = Interact :|: Login :|: NilDSL 27 val PRG = DSL.Make[PRG] 28 29 val authenticDSL: Free[PRG.Cop, Boolean] = 30 for { 31 uid <- Ask("Enter your user id:").freek[PRG] 32 pwd <- Ask("Enter password:").freek[PRG] 33 auth <- Authenticate(uid,pwd).freek[PRG] 34 } yield auth 35 36 val interactLoginDSL: Free[PRG.Cop, Unit] = 37 for { 38 uid <- Ask("Enter your user id:").freek[PRG] 39 pwd <- Ask("Enter password:").freek[PRG] 40 auth <- Authenticate(uid,pwd).freek[PRG] 41 _ <- if (auth) Tell(s"Hello $uid, welcome to the zoo!").freek[PRG] 42 else Tell(s"Sorry, Who is $uid?").freek[PRG] 43 } yield () 44 45 import Auth._ 46 type PRG3 = Auth :|: PRG //Interact :|: Login :|: NilDSL 47 val PRG3 = DSL.Make[PRG3] 48 val authorizeDSL: Free[PRG3.Cop, Unit] = 49 for { 50 uid <- Ask("Enter your User ID:").freek[PRG3] 51 pwd <- Ask("Enter your Password:").freek[PRG3] 52 auth <- Authenticate(uid,pwd).freek[PRG3] 53 perm <- if (auth) Authorize(uid).freek[PRG3] 54 else Free.pure[PRG3.Cop,Boolean](false) 55 _ <- if (perm) Tell(s"Hello $uid, access granted!").freek[PRG3] 56 else Tell(s"Sorry $uid, access denied!").freek[PRG3] 57 } yield() 58 59 } 60 object IMPLs { 61 import ADTs._ 62 import Interact._ 63 import Login._ 64 val idInteract = new (Interact ~> Id) { 65 def apply[A](ia: Interact[A]): Id[A] = ia match { 66 case Ask(p) => {println(p); scala.io.StdIn.readLine} 67 case Tell(m) => println(m) 68 } 69 } 70 val idLogin = new (Login ~> Id) { 71 def apply[A](la: Login[A]): Id[A] = la match { 72 case Authenticate(u,p) => (u,p) match { 73 case ("Tiger","123") => true 74 case _ => false 75 } 76 } 77 } 78 val interactLogin = idInteract :&: idLogin 79 import Dependencies._ 80 type ReaderContext[A] = Reader[Authenticator,A] 81 object readerInteract extends (Interact ~> ReaderContext) { 82 def apply[A](ia: Interact[A]): ReaderContext[A] = ia match { 83 case Ask(p) => Reader {pc => {println(p); scala.io.StdIn.readLine}} 84 case Tell(m) => Reader {_ => println(m)} 85 } 86 } 87 object readerLogin extends (Login ~> ReaderContext) { 88 def apply[A](la: Login[A]): ReaderContext[A] = la match { 89 case Authenticate(u,p) => Reader {pc => pc.matchUserPassword(u,p)} 90 } 91 } 92 val userInteractLogin = readerLogin :&: readerInteract 93 94 val readerAuth = new (Auth ~> ReaderContext) { 95 def apply[A](aa: Auth[A]): ReaderContext[A] = aa match { 96 case Authorize(u) => Reader {ac => ac.grandAccess(u)} 97 } 98 } 99 val userAuth = readerAuth :&: userInteractLogin 100 } 101 102 } 103 object Dependencies { 104 trait PasswordControl { 105 val mapPasswords: Map[String,String] 106 def matchUserPassword(uid: String, pswd: String): Boolean 107 } 108 trait AccessControl { 109 val mapAccesses: Map[String, Boolean] 110 def grandAccess(uid: String): Boolean 111 } 112 trait Authenticator extends PasswordControl with AccessControl 113 } 114 115 object freeKDemo extends App { 116 import FreeKModules._ 117 import DSLs._ 118 import IMPLs._ 119 // val r0 = authenticDSL.foldMap(interactLogin.nat) 120 // val r = authenticDSL.interpret(interactLogin) 121 import Dependencies._ 122 object AuthControl extends Authenticator { 123 override val mapPasswords = Map( 124 "Tiger" -> "1234", 125 "John" -> "0000" 126 ) 127 override def matchUserPassword(uid: String, pswd: String) = 128 mapPasswords.getOrElse(uid, pswd+"!") == pswd 129 130 override val mapAccesses = Map ( 131 "Tiger" -> true, 132 "John" -> false 133 ) 134 override def grandAccess(uid: String) = 135 mapAccesses.getOrElse(uid, false) 136 } 137 138 // interactLoginDSL.interpret(userInteractLogin).run(AuthControl) 139 authorizeDSL.interpret(userAuth).run(AuthControl) 140 }