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,形成一个多语法的语句集合。具体的语法集合以及多语法的效果实现对应运算可以参考前面这篇博客中的讨论

时间: 2024-10-11 12:05:56

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

Scalaz(54)- scalaz-stream: 函数式多线程编程模式-Free Streaming Programming Model

长久以来,函数式编程模式都被认为是一种学术研究用或教学实验用的编程模式.直到近几年由于大数据和多核CPU的兴起造成了函数式编程模式在一些实际大型应用中的出现,这才逐渐改变了人们对函数式编程无用论的观点.通过一段时间对函数式编程方法的学习,我们了解到Free Monad的算式/算法关注分离(separation of concern)可以是一种很实用的函数式编程模式.用Free Monad编写的程序容易理解并具备良好的可维护性.scalaz-stream的流程控制和多线程运算模式可以实现程序的安全

Scalaz(10)- Monad:就是一种函数式编程模式-a design patter

Monad typeclass不是一种类型,而是一种程序设计模式(design pattern),是泛函编程中最重要的编程概念,因而很多行内人把FP又称为Monadic Programming.这其中透露的Monad重要性则不言而喻.Scalaz是通过Monad typeclass为数据运算的程序提供了一套规范的编程方式,如常见的for-comprehension.而不同类型的Monad实例则会支持不同的程序运算行为,如:Option Monad在运算中如果遇到None值则会中途退出:State

关于C51与汇编的实用混合编程

最近研究了下51的混合编程,总结一下吧! 1.生成C51的汇编源码 右键单击项目文件,在弹出的opinion for file ....选择上生成SRC文件,这个可以产生汇编源文件,也能允许在C51中嵌入A51,即汇编代码,另外不知为何在项目中若嵌入汇编需添加keil的C51S.LIB,否则下载到单片机后无法正常运行,至于具体原因,现在还不是很明白. 2.查看SRC汇编文件 在项目文件中可以找到SRC文件,可查看所写的C51代码翻译成汇编后的代码,截取部分如图所示,由于编译器在编译C文件时会加入

有趣实用的编程网站集合

前言 此文是我从互联网各个地方收集到的一些有趣且实用的编程相关网站集合,会尽可能列出引用的出处(持续收集中...) 列表 1. Hello World大全 收集了由各种语言写成的hello world程序 2.Font Awesome 包含大量开源且免费的web图标 3.IT ebooks IT相关电子书,可下载 4.OverApi 各种语言技术相关的API 5.DevDocs 综合类在线文档 6.免费的中文编程书籍索引 7.Oreilly免费电子书 8.CodeWars 在线编程刷题网站 9.

CUDA 标准编程模式

前言 本文将介绍 CUDA 编程的基本模式,所有 CUDA 程序都基于此模式编写,即使是调用库,库的底层也是这个模式实现的. 模式描述 1. 定义需要在 device 端执行的函数.( 函数声明前加 _golbal_ 关键字 ) 2. 在显存中为待运算的数据以及需要存放结果的变量开辟显存空间.( cudaMalloc 函数实现 ) 3. 将待运算的数据传输进显存.( cudaMemcpy,cublasSetVector 等函数实现 ) 4. 调用 device 端函数,同时要将需要为 devic

《游戏编程模式》(8)

<游戏编程模式>最后一篇,刚从英国玩了一圈,春节又要到啦 Chapter 19 对象池 使用固定的对象池重用对象,取代单独地分配和释放对象,达到提升性能和优化内存使用的目的. 使用情境: 频繁创建销毁对象: 对象大小基本一致: 堆上分配内存较慢或可能产生内存碎片: 粒子类: 用union节省内存:粒子使用时用live结构体,不使用时用next指针 1 class Particle 2 { 3 4 public: 5 Particle() 6 : framesLeft_(0) 7 {} 8 9

编程模式之十四----行为型----职责链模式

定义 避免把一个请求的发送者和接收者耦合在一起,使多个对象都有机会处理请求.将这个请求的多个接收着连接成一条链,并让请求沿着这个链传递下去,只到有一个结点能处理请求. 职责链模式中,链形成后,不一定非要有一个结点能够处理请求,也就是说,所有结点都可以处理一下再往下传,也可以都不处理,这样说来就比较灵活了. 组成 抽象处理者:AbstractHandler.维护一个对自身类型对象的引用,这个引用作为下一个具体的处理者.声明所有处理者都用到的方法,这个方法的作用是设置本处理者后面的一个处理者.另外声

利用MVC编程模式-开发一个简易记事本app

学了极客学院一个开发记事本的课程,利用自己对MVC编程模式的简单理解重写了一遍该app. github地址:https://github.com/morningsky/MyNote MVC即,模型(model)-视图(view)-控制器(controller),有效的实现了数据-业务逻辑-视图显示的代码分离,使得加入新功能时不需要重新编写业务逻辑,大大提高了代码的可维护性. 在这个案列中,一开始只是开发了添加文字内容的记事功能,添加图片功能时在activity文件中写入imageview的逻辑

C#编程模式之扩展命令

C#编程模式之扩展命令 前言 根据上一篇的命令模式和在工作中遇到的一些实际情况,有了本篇文章,时时都是学习的一个过程,会在这个过程中发现许多好的模式或者是一种开发方式,今天写出来的就是我工作中常用到的,自己感觉这种方式很优雅很漂亮,就自己试着实现了一下,可能原框架中不是这样的,有许多不足之处还请大家指点. 需求 我还不清楚这种方式是模式还是框架开发中用到的技术,我暂且叫它为命令控制器吧. 命令控制器的主要功能就是获取用户提供的命令,然后来执行命令. 在这里我把要执行的"命令"设计成一个