在任何模式的编程过程中都无法避免副作用的产生。我们可以用F[A]这种类型模拟FP的运算指令:A是可能产生副作用的运算,F[_]是个代数数据类型ADT(Algebraic Data Type),可以实现函数组合(functional composition),我们可以不用理会A,先用F[_]来组合形成描述功能的抽象程序AST(Abstract Syntax Tree),对A的运算可以分开另一个过程去实现,而且可以有多种的运算实现方式,这样就达到了算式AST(Monadic Programming)、算法(Interpretation)的所谓关注分离(separation of concern)目的。在前面的讨论中我们介绍过:我们可以把任何F[A]升格成Monad,而Monad具备最完善的函数组合性能,特别是它支持for-comprehension这种表达方式。我们可以在for-comprehension框架里进行我们熟悉的行令编程(imperative programming),可以使程序意思表达更加显而易见。
下面我们来做一个简单的示范:模拟一个互动智力算数测试(math quiz):在系统提示下,用户输入第一个数字、再输入第二个数字、再输入操作符号、系统输出算数操作结果。我们可以设计ADT如下:
sealed trait Quiz[+Next] case class Question[Next](que: String, n: String => Next) extends Quiz[Next] case class Answer[Next](ans: String, n: Next) extends Quiz[Next]
我们可以map over Next类型获取Quiz的Functor实例:
implicit object QFunctor extends Functor[Quiz] { def map[A,B](qa: Quiz[A])(f: A => B): Quiz[B] = qa match { case q: Question[A] => Question(q.que, q.n andThen f) case Answer(a,n) => Answer(a,f(n)) } }
我们再来几个操作帮助方法:
//操作帮助方法helper methods def askNumber(q: String) = Question(q, (inputString => inputString.toInt)) //_.toInt def askOperator(q: String) = Question(q, (inputString => inputString.head.toUpper.toChar)) def answer(fnum: Int, snum: Int, opr: Char) = { def result = opr match { case 'A' => fnum + snum case 'M' => fnum * snum case 'D' => fnum / snum case 'S' => fnum - snum } Answer("my answer is: " + result.toString,()) }
import Quiz._ val prg = for { fn <- askNumber("The first number is:") sn <- askNumber("The second number is:") op <- askOperator("The operation is:") _ <- answer(fn,sn,op) } yield() //> prg : scalaz.Free[Exercises.interact.Quiz,Unit] = Gosub()
implicit def quizToFree[A](qz: Quiz[A]): Free[Quiz,A] = Free.liftF(qz)
sealed trait Quiz[+Next] object Quiz { //问题que:String, 等待String 然后转成数字或操作符号 case class Question[Next](que: String, n: String => Next) extends Quiz[Next] case class Answer[Next](ans: String, n: Next) extends Quiz[Next] implicit object QFunctor extends Functor[Quiz] { def map[A,B](qa: Quiz[A])(f: A => B): Quiz[B] = qa match { case q: Question[A] => Question(q.que, q.n andThen f) case Answer(a,n) => Answer(a,f(n)) } } //操作帮助方法helper methods def askNumber(q: String) = Question(q, (inputString => inputString.toInt)) //_.toInt def askOperator(q: String) = Question(q, (inputString => inputString.head.toUpper.toChar)) //_.head.toUpper.toChar def answer(fnum: Int, snum: Int, opr: Char) = { def result = opr match { case 'A' => fnum + snum case 'M' => fnum * snum case 'D' => fnum / snum case 'S' => fnum - snum } Answer("my answer is: " + result.toString,()) } implicit def quizToFree[A](qz: Quiz[A]): Free[Quiz,A] = Free.liftF(qz) } import Quiz._ val prg = for { fn <- askNumber("The first number is:") sn <- askNumber("The second number is:") op <- askOperator("The operation is:") _ <- answer(fn,sn,op) } yield() //> prg : scalaz.Free[Exercises.interact.Quiz,Unit] = Gosub()
再看看下面的例子。试着猜测程序的作用:
sealed trait Calc[+A] object Calc { case class Push(value: Int) extends Calc[Unit] case class Add() extends Calc[Unit] case class Mul() extends Calc[Unit] case class Div() extends Calc[Unit] case class Sub() extends Calc[Unit] implicit def calcToFree[A](ca: Calc[A]) = Free.liftFC(ca) } import Calc._ val ast = for { _ <- Push(23) _ <- Push(3) _ <- Add() _ <- Push(5) _ <- Mul() } yield () //> ast : scalaz.Free[[x]scalaz.Coyoneda[Exercises.interact.Calc,x],Unit] = Gosub()