首页 > 代码库 > Cats(3)- freeK-Free编程更轻松,Free programming with freeK

Cats(3)- freeK-Free编程更轻松,Free programming with freeK

   在上一节我们讨论了通过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 :|: NilDSL17     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 auth25   }

从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") => true15           case _ => false16         }17       }18     }19     val interactLogin = idInteract :&: idLogin20   }

这部分没有什么变化。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): Boolean5   }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+"!") == pwd15   }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 AccessControl11 }

我们用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" -> false13     )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 John11 Enter your Password:12 000013 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 :&: userInteractLogin100   }101 102 }103 object Dependencies {104   trait PasswordControl {105     val mapPasswords: Map[String,String]106     def matchUserPassword(uid: String, pswd: String): Boolean107   }108   trait AccessControl {109     val mapAccesses: Map[String, Boolean]110     def grandAccess(uid: String): Boolean111   }112   trait Authenticator extends PasswordControl with AccessControl113 }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+"!") == pswd129 130     override val mapAccesses = Map (131       "Tiger" -> true,132       "John" -> false133     )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 }

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Cats(3)- freeK-Free编程更轻松,Free programming with freeK