在上一节我们讨论了通过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 }
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/12888.html