Scalaz(43)- 总结 :FP就是实用的编程模式

时间:2022-06-23 19:09:38

完成了对Free Monad这部分内容的学习了解后,心头豁然开朗,存在心里对FP的疑虑也一扫而光。之前也抱着跟大多数人一样的主观概念,认为FP只适合学术性探讨、缺乏实际应用、运行效率低,很难发展成现实的软件开发模式。Free Monad的出现恰恰解决我心中的疑问,更正了我对FP的偏见:Free Monad提供了一套在Monad 算法内(在 for-comprehension内)的行令编程(imperative programming)方法,解决了FP的复杂语法,使Monadic编程更贴近传统编程模式的习惯和思维,程序意图更容易理解。Free Monad的函数结构化(reification)有效解决了递归算法造成的堆栈溢出(*)问题,使FP程序能够安全运行,实现在现实中的应用。

在学习scalaz初期,FP的类型和函数施用搞得我很无奈,不适应:FP类型的Functor,Applicative,Monad等等给我的印象是无比抽象的。而且接触到的有关这些类型的具体使用例子又大多数是针对List,Option,Map这些教科书通用类型的,感觉FP就是一种对编程模式的学术探讨,是用来改变思想的,没什么实用价值。当然,FP的递归算法又更加深了我们对现实中选用它的疑虑。但从Free Monad反向回顾scalaz的这些基础类型和函数,我好像渐渐地明白了它们在scalaz这个FP工具库中存在的意义。回到我了解scalaz的目的:就是希望证实FP这种模式可以成为一种生产工具。之前已经了解了FP模式的优势但对于它的实际应用还是存有疑虑。以我粗浅的标准来讲,如果作为一种实际可用的编程语言,起码必须具备以下几点:

1、语法简单,容易掌握

2、表达式简洁、直白

3、能够保证运行安全

试想我们如何能长期的编写fa.flatMap(a => fb.flatMap(b => fc.map(...)))这样的程序呢?FP针对泛函结构F[A]的运算有着一套全新的数据结构和函数施用方式,没人能明白这样的程序表达的到底是什么目的。这时我们遇到了flatMap函数的方法糖for-comprehension,它可用让我们在一个for-loop里进行我们熟悉的行令式编程,就像下面这样:

for {
x <- getRecNo
r <- getRecord(x)
_ <- r.save()
} yield ()

除去for-yield后不就是我们熟悉的编程方式吗?我们已经习惯并掌握了这种编程方式。因为flatMap是Monad的运算函数,所以FP式的编程又被称为Monadic Programming,直白来讲就是用Monad来编程,或者就是在一个Monad壳子(context)里编程。可以说scalaz的所有东西最终都和Monad有关(everything is about Monad)。通过证明,任何Monad都必须是Functor和Applicative,所以在scalaz里提供的Functor,Applicative以及其它的基础typeclass并不如我们想象的那样好像没什么实用价值,实际上scalaz是通过这些基础typeclass为我们构建各种功能的Monad提供了支持的。现在看来这些基础typeclass还是值得了解的。而且看来如果要进行FP编程,就必须先掌握Monad应用,因为我们需要把所有东西都升格成Monad。那么Monad真的像许多人感觉的那样神秘、虚渺、触不可及吗?答案是否定的。接触的多了我们就可以了解Monad的主要作用就是把一个算法,无论是一个值或者一个函数升格成Monad,这样我们就可以在Monad-for-comprehension里使用它们了。看看scalaz里一些类型的Monad格式吧:

case class State (run: S => (A,S))
case class Reader(run: A => B)
case class Writer(run: (W, A))
...

它们都是把普通的函数或者运算包嵌在一个结构里然后在实现这个类型的flatMap函数时体现这些运算的具体意义。这些道理在scalaz的源代码里都可以得到证实。所以我们根本不需要畏惧Monad,应该采取积极态度去充分了解掌握它。我印象中比较麻烦的是Monad转换和功能结合,它们都涉及到类型匹配,需要较大的想象空间。

好了,有了Monad和各种功能转换、集合方式,我们可以在for-comprehension里进行熟悉的编程了。那么会不会出现在一个for-loop里出现几百行指令的情况呢?我认为不会,因为我们可以用函数组合方式把一个大程序分解成各种功能单一的简单函数,然后逐层进行组合,最终的程序最多也就是十几二十行。这种组合特性有赖于Free Monad提供的算式/算法关注分离(program/interpret separation of concern)模式。它可以把影响函数组合的副作用放到算法(interpret)阶段,让我们能够在算式中实现程序间的组合。这个我用以下的代码来示范一下:

val prgGetData = for {
x <- getRecNo
r <- getRecord(x)
} yield r val prgUpdateRecord = for {
x <- getData
r <- prgGetData
- <- r.updateAndSave()
} yield () prgUpdateRecord.run(dbActions)

再有一个问题就是FP的运算方式了:我们可以看到运算一连串的flatMap是一种递归算法,除非使用尾递归算法,compiler是无法对算法进行优化的,那么运算flatMap就很容易会发生堆栈溢出错误(* error),无法保障程序运行安全。Free Monad是通过函数结构化,既是把flatMap函数作为一种数据存放在heap内存上,然后通过折叠算法逐个运算,这和传统的函数引用方式:即通过堆栈设置运算环境有根本不同,Free Monad是用heap换stack,避免了递归算法容易出现的堆栈溢出问题。这方面又解决了FP程序运行安全问题。

通过调研、演练后基本掌握了Monadic Programming(MP)的方式方法。现在把它总结如下:

MP编程可分三个环节:

1、编写程序功能描述,是一串代数语法(AST)。不是即时程序(Programm)

2、把功能描述对应到具体的效果实现方式

3、最后,运算选定的实现方式

分成具体的步骤如下:

1、ADT:模拟语句,用F[A]类数据类型来模拟指令

object FreeADTs {
trait Dialog[A]
case class Ask(prompt: String) extends Dialog[String]
case class Tell(msg: String) extends Dialog[Unit]
implicit def dialogToFree[A](da: Dialog[A]) = Free.liftF(da)
}

2、lift:把F[A]升格成Free Monad

  implicit def dialogToFree[A](da: Dialog[A]) = Free.liftF(da)

3、AST:代数程序,描述程序功能

object FreeASTs {
import FreeADTs._
val prg: Free[Dialog,Unit] = for {
x <- Ask("What's your first name?")
_ <- Tell(s"Hi, $x")
y <- Ask("What's your last name?")
_ <- Tell(s"Hello $x $y!!!")
} yield()
}

4、Interpret:把F[A]对应到G[A]上。G[A]是实现具体效果的Monad。以下提供了两种不同效果的实现方式

object FreeInterp {
import FreeADTs._
object DialogConsole extends (Dialog ~> Id) {
def apply[A](da: Dialog[A]): Id[A] = da match {
case Ask(p) => println(p); readLine
case Tell(s) => println(s) }
}
type WF[A] = Map[String,String] => A
type Tester[A] = WriterT[WF,List[String],A]
implicit val testerMonad = WriterT.writerTMonad[WF,List[String]]
def testerToWriter[A](f: Map[String,String] => (List[String],A)) = WriterT[WF,List[String],A](f)
object DialogTester extends (Dialog ~> Tester) {
def apply[A](da: Dialog[A]): Tester[A] = da match {
case Ask(p) => testerToWriter {m => (List(),m(p))}
case Tell(s) => testerToWriter {m => (List(s),())}
}
}
}

5、Run:最后,对实现方式进行运算

object SimpleFree extends App {
import FreeASTs._
import FreeInterp._ //prg.foldMapRec(DialogConsole)
prg.foldMapRec(DialogTester).run(
Map("What's your first name?" -> "Johnny", "What's your last name?" -> "Foo")
)._1.map(println) }

如果出现多种模拟语法的情况,我们可以用inject方式把各种语法注入Coproduct,形成一个多语法的语句集合。具体的语法集合以及多语法的效果实现对应运算可以参考前面这篇博客中的讨论