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

  完成了对Free Monad这部分内容的学习了解后,心头豁然开朗,存在心里对FP的疑虑也一扫而光。之前也抱着跟大多数人一样的主观概念,认为FP只适合学术性探讨、缺乏实际应用、运行效率低,很难发展成现实的软件开发模式。Free Monad的出现恰恰解决我心中的疑问,更正了我对FP的偏见:Free Monad提供了一套在Monad 算法内(在 for-comprehension内)的行令编程(imperative programming)方法,解决了FP的复杂语法,使Monadic编程更贴近传统编程模式的习惯和思维,程序意图更容易理解。Free Monad的函数结构化(reification)有效解决了递归算法造成的堆栈溢出(stackoverflow)问题,使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就很容易会发生堆栈溢出错误(stackoverflow 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,形成一个多语法的语句集合。具体的语法集合以及多语法的效果实现对应运算可以参考前面这篇博客中的讨论

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java闲聊

设计模式(总纲)

网上关于设计模式的文章很多,为什么我还要重复写设计模式的文章,在看到LZ大牛的一篇《聊聊阿里社招面试,谈谈“野生”Java程序员学习的道路》博文后,发现自己陷入...

521
来自专栏C语言及其他语言

C语言中的宏陷阱 #define SQU(x) x*x

有同学写过或者想写这样的宏定义吗? 求两个或几个数的乘积: #define SQU(x) x*x 我们正常使用没有问题: ? 但如果这样写呢? ? 哎呀,...

3405
来自专栏较真的前端

看到这题后还敢说自己精通Promise吗?

1482
来自专栏嵌入式程序猿

宏函数使用的陷阱

在嵌入式软件设计中,有工程师经常会定义一些宏函数,宏函数的使用虽然可以减少开销,但是宏函数的使用一定要小心,例如我们定义一个求取两个数中的较大者的宏函数来做试验...

3229
来自专栏PHP在线

数据类型和Json格式

作者: 阮一峰 1. 前几天,我才知道有一种简化的数据交换格式,叫做yaml。 我翻了一遍它的文档,看懂的地方不多,但是有一句话令我茅塞顿开。 它说,从结构上看...

33810
来自专栏专知

【LeetCode 20】关关的刷题日记45 – Valid Parenthese

关关的刷题日记45 – Leetcode 20. Valid Parenthese 题目 Given a string containing just the ...

3407
来自专栏海天一树

NOIP 2018普及组初赛C/C++答案详解

1 D 打印机是把电脑里的资料打印到纸上,所以是输出设备。 扫描仪、键盘和鼠标都是往电脑里输入东西,是输入设备。

1373
来自专栏编程一生

专治不会看源码的毛病--spring源码解析AOP篇

1225
来自专栏程序员与猫

使用抽象类和接口的优解

1. 前言 笔者相信,每个使用面向对象语言的开发者自编码以来,肯定便琢磨过抽象类(Abstract)和接口(Interface)的区别。可能一些人已经找到了适...

2025
来自专栏AzMark

Python基础知识

1244

扫码关注云+社区