泛函编程(30)-泛函IO:Free Monad-Monad生产线
在上节我们介绍了Trampoline。它主要是为了解决堆栈溢出(StackOverflow)错误而设计的。Trampoline类型是一种数据结构,它的设计思路是以heap换stack:对应传统递归算法运行时在堆栈上寄存程序状态,用Trampoline进行递归算法时程序状态是保存在Trampoline的数据结构里的。数据结构是在heap上的,所以可以实现以heap换stack的效果。这种以数据结构代替函数调用来解决问题的方式又为泛函编程提供了更广阔的发展空间。
我们知道,任何涉及IO的运算都会面临堆栈溢出问题。这是因为IO通常针对无法预计的数据量以及重复循环操作。所以IO算法设计也会采用与Trampoline一样的数据结构。或者我们应该沿用Trampoline数据结构和算法来设计IO组件库。如此思考那么我们就必须对Trampoline进行深度抽象了。Free Monad就是Trampline的延伸。在介绍Free Monad之前我们先从一个现实的例子来展开讨论:
假设我们要编写一个银行转账的函数,我们可能先把这个函数的款式(function signature)推导出来:
- def transfer(amount: Double, from: Account, to: Account, user: User,
- context: Authorization with Logger with ErrorHandler with Storage): Unit
首先我们在这里采用了参数注入(parameter injection)方式:在transfer函数输入参数中注入context object。这个context object里包括了身份验证、操作跟踪、错误处理、数据存取等等。这算是传统OOP编程模式吧。对于一个泛函编程人员来讲:通过这个context object 可以进行一系列的操作。包括IO操作,也就是说可以进行一些含有副作用(side effect)的操作。那么这个函数是无法实现函数组合(function composition)。transfer函数就不是一个泛函编程人员该使用的函数了。
也许我们应该从泛函编程角度来尝试设计这个函数:用泛函编程提倡的不可蜕变(immutability)方式来设计,也就是向函数调用方返回一些东西。
比如我们可以向函数调用方返回一个描述操作的程序:一串命令(instruction):
- def transfer(amount: Double, from: Account, to: Account, user: User): List[Instruction]
这个版本肯定是个泛函版本了。不过假如Instruction类型包括了互动操作的话就不足够了。我们先看看简单的交互的数据类型:
- trait Interact[A] //交互数据类型
- //提问,等待返回String类型答案
- case class Ask(prompt: String) extends Interact[String]
- //告知,没有返回结果
- case class Tell(msg: String) extends Interact[Unit]
如果我们按照上面的思路返回一串命令的话:
- val prg = List(Ask("What's your first name?"),
- Ask("What's your last name?"),
- Tell("Hello ??? ???"))
这个程序prg是有缺陷的:无法实现交互。好像如果能把Ask指令存放到一个临时变量里就可以达到目的了。那么如果我们把这个prg改写成下面这样:
- for {
- x <- Ask("What's your first name?")
- y <- Ask("What's your last name?")
- _ <- Tell(s"Hello $y $x!")
- } yield ()
这不就是Monad款式吗?原来解决方法就是把交互类型trait Interact[A]变成Monad就行了。
不过要把Interact变成Monad就必须实现unit和flatMap两个函数,检查Interact trait,明显这是不可能的。
那我们把下面的努力都应该放在如何转变成Monad这方面了。既然我们在本篇命题里提到Free Monad是Monad生产线。那么用Free Monad能不能把Interact变成Monad呢?
我们先看看这个Free Monad类型结构:
- trait Free[F[_],A]
- case class Return[F[_],A](a: A) extends Free[F,A]
- case class Bind[F[_],I,A](a: F[I], f: I => Free[F,A]) extends Free[F,A]
这个Free结果跟Trampoline简直是太相似了。如果Free是个Monad,那么我们应该必须实现它的flatMap函数:
- trait Free[F[_],A] {
- def unit(a: A) = Return(a)
- def flatMap[B](f: A => Free[F,B]): Free[F,B] = this match {
- case Return(a) => f(a)
- //还记得Trampoline FlatMap(FlatMap(b,g),f) == FlatMap(b, (x: Any) => FlatMap(g(x),f))
- case Bind(fa,g) => Bind(fa, (x: Any) => g(x) flatMap f)
- //下面采用了函数组合方式。具备同样功能
- // case Bind(fa,g) => Bind(fa, g andThen (_ flatMap f))
- }
- def map[B](f: A => B): Free[F,B] = flatMap(a => Return(f(a)))
- }
- case class Return[F[_],A](a: A) extends Free[F,A]
- case class Bind[F[_],I,A](a: F[I], f: I => Free[F,A]) extends Free[F,A]
我们可以用下面的lift函数来把Interact[A]升格成Free[F,A] :
- implicit def lift[F[_],A](fa: F[A]): Free[F,A] = Bind(fa, (a: A) => Return(a))
- //> lift: [F[_], A](fa: F[A])ch13.ex6.Free[F,A]
有了lift我们可以吧prg升格成Monad:
- trait Interact[A] //交互数据类型
- //提问,等待返回String类型答案
- case class Ask(prompt: String) extends Interact[String]
- //告知,没有返回结果
- case class Tell(msg: String) extends Interact[Unit]
- implicit def lift[F[_],A](fa: F[A]): Free[F,A] = Bind(fa, (a: A) => Return(a))
- //> lift: [F[_], A](fa: F[A])ch13.ex6.Free[F,A]
- for {
- x <- Ask("What's your first name?")
- y <- Ask("What's your last name?")
- _ <- Tell(s"Hello $y $x!")
- } yield () //> res0: ch13.ex6.Free[ch13.ex6.Interact,Unit] = Bind(Ask(What's your first nam
- //| e?),<function1>)
这是因为implicit scope里的类型转换使Interact升格为Free,而Free是个Monad,所以我们可以使用for-comprehension。
好了,这个程序描述完成后应该如何运算呢?Free Monad包括了两部分功能,相互之间无关联,可以分开单独考虑。这就是所谓的关注分离(separation of concern)。Free Monad的两项功能分别是Monad,和Interpreter(解译器)。我们用Monad描述程序算法,用Interpreter解译程序形成针对特定运行环境的可运行代码。
Free Monad的Interpreter实现了算法和运算的分离考虑。Interpreter程序运算是通过一个转换函数实现的。这个函数把F[_]这样一个算法解译成G[_]这样一个针对可运行环境的Monad运行代码。这种转换就是自然转换(Natural Transformation),它的函数款式如下:
- trait ~>[F[_],G[_]] {
- def apply[A](fa: F[A]): G[A]
- }
很明显,这个构建函数(constructor)把传入的F[A]解译成G[A]。
现在Interpreter运行一段算法就是对算法F[_]中的表达式进行一对一的G[_]转换。就像对List结构中元素进行处理的方式一样,我们可以用折叠算法来实现F[_]结构中表达式的转换:
- def foldMap[G[_]: Monad](f: F ~> G): G[A] = this match {
- case Return(a) => Monad[G].unit(a)
- case Bind(b,g) => Monad[G].flatMap(f(b))(a => g(a).foldMap(f))
- }
我们看到,foldMap把Free Monad F[_]中的表达式与Monad G状态进行了对应。注意Bind状态是循环递归的。
现在我们可以试试最简单的解译:F,Id转换:
- ype Id[A] = A
- implicit val idMonad: Monad[Id] = new Monad[Id] {
- def unit[A](a: A) = a
- def flatMap[A,B](fa: A)(f: A => B): B = f(fa)
- } //> idMonad : ch13.ex6.Monad[ch13.ex6.Id] = ch13.ex6$$anonfun$main$1$$anon$1@2
- //| 530c12
- object Console extends (Interact ~> Id) {
- def apply[A](i: Interact[A]): A = i match {
- case Ask(prompt) => {
- println(prompt)
- readLine
- }
- case Tell(msg) => println(msg)
- }
- }
运算上面那段Interact程序:由于Id不产生任何效果,Interact到Id转换即是直接运算Interact表达式:
- val prg = for {
- x <- Ask("What's your first name?")
- y <- Ask("What's your last name?")
- _ <- Tell(s"Hello $y $x!")
- } yield () //> prg : ch13.ex6.Free[ch13.ex6.Interact,Unit] = Bind(Ask(What's your first n
- //| ame?),<function1>)
- prg.foldMap(Console)
或者我们可以试试再复杂一点的解译:
- type Tester[A] = Map[String, String] => (List[String], A)
- implicit val testerMonad = new Monad[Tester] {
- def unit[A](a: A) = (_ => (List(),a))
- def flatMap[A,B](ta: Tester[A])(f: A => Tester[B]): Tester[B] = {
- m => {
- val (l1,a) = ta(m)
- val (l2,b) = f(a)(m)
- (l1 ++ l2,b)
- }
- }
- } //> testerMonad : ch13.ex6.Monad[ch13.ex6.Tester]{def unit[A](a: A): Map[Strin
- //| g,String] => (List[Nothing], A)} = ch13.ex6$$anonfun$main$1$$anon$2@5b464ce
- //| 8
- object TestConsole extends (Interact ~> Tester) {
- def apply[A](i: Interact[A]): Tester[A] = i match {
- case Ask(prompt) => m => (List(), m(prompt))
- case Tell(msg) => _ => (List(msg),())
- }
- }
以上我们把运行Interact中的交互信息存入Map[String,String]结构中。在这里进行了Interact到一个函数Map=>(List,A)的转换。
- val prg = for {
- x <- Ask("What's your first name?")
- y <- Ask("What's your last name?")
- _ <- Tell(s"Hello $y $x!")
- } yield () //> prg : ch13.ex6.Free[ch13.ex6.Interact,Unit] = Bind(Ask(What's your first n
- //| ame?),<function1>)
- prg.foldMap(TestConsole)
在上一节我们讨论了Trampoline。主要目的是解决泛函算法中不可避免的堆栈溢出问题。如果我们用Free Monad来解决IO问题的话,堆栈溢出问题也是无法避免的。我们应该考虑在Free Monad里使用Trampoline类型。这样我们才可以放心地用Free Monad来产生任何类型的Monad并在运算中以heap换stack解决堆栈溢出问题。
泛函编程(30)-泛函IO:Free Monad-Monad生产线的更多相关文章
- 泛函编程(27)-泛函编程模式-Monad Transformer
经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...
- 泛函编程(5)-数据结构(Functional Data Structures)
编程即是编制对数据进行运算的过程.特殊的运算必须用特定的数据结构来支持有效运算.如果没有数据结构的支持,我们就只能为每条数据申明一个内存地址了,然后使用这些地址来操作这些数据,也就是我们熟悉的申明变量 ...
- 泛函编程(32)-泛函IO:IO Monad
由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...
- 泛函编程(23)-泛函数据类型-Monad
简单来说:Monad就是泛函编程中最概括通用的数据模型(高阶数据类型).它不但涵盖了所有基础类型(primitive types)的泛函行为及操作,而且任何高阶类或者自定义类一旦具备Monad特性就可 ...
- 泛函编程(34)-泛函变量:处理状态转变-ST Monad
泛函编程的核心模式就是函数组合(compositionality).实现函数组合的必要条件之一就是参与组合的各方程序都必须是纯代码的(pure code).所谓纯代码就是程序中的所有表达式都必须是Re ...
- 泛函编程(28)-粗俗浅解:Functor, Applicative, Monad
经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点:我是指在现实编程的情况下所谓的泛函编程到底如何特别.我们已经习惯了传统的行令式编程(imperative progra ...
- 泛函编程(25)-泛函数据类型-Monad-Applicative
上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...
- 泛函编程(24)-泛函数据类型-Monad, monadic programming
在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...
- 备份-泛函编程(23)-泛函数据类型-Monad
泛函编程(23)-泛函数据类型-Monad http://www.cnblogs.com/tiger-xc/p/4461807.html https://blog.csdn.net/samsai100 ...
随机推荐
- Atitti usrQBf1801 翻页控件规范 v2
Atitti usrQBf1801 翻页控件规范 v2 1. 参考api 参考easyui ,.net系列的1 1.1. 翻页流程 初始化翻页控件,以及绑定新页面event onSelectPa ...
- Linux初学 - head,tail,grep,sed,yum,find
head 查看文件头部 -n 指定查看行数 默认10行 tail 查看文件尾部 n 指定查看行数 默认10行 Grep 命令 用法大全 . 参数: -I :忽略大小写 -c :打印匹配的行数 -l : ...
- 转【】浅谈sql中的in与not in,exists与not exists的区别_
浅谈sql中的in与not in,exists与not exists的区别 1.in和exists in是把外表和内表作hash连接,而exists是对外表作loop循环,每次loop循环再对内表 ...
- 第五章 运输层(UDP和TCP三次握手,四次挥手分析)
序言 通过这章,可以知道其实三次握手和四次挥手其实真的好简单,通过这章的学习,我相信你也会同样的认为,以后在也不需要听到别人问三次握手的过程而自己一脸懵逼了,觉得人家好屌,其实也就是他懂你不懂,仅 ...
- web前端攻击详解
前端攻击成因 在web网页的脚本中,有些部分的显示内容会依据外界输入值而发生变化,而如果这些声称html的程序中存在问题,就会滋生名为跨站脚本的安全隐患 XSS跨站脚本攻击: 英文全称cross-si ...
- python socket 学习
Python在网络通讯方面功能强大,今天学习一下Socket通讯的基本方式,分别是UDP通讯和TCP通讯. UDP通讯 upd 服务端 #!/usr/bin/env python # -*- codi ...
- ProgressBar 源码
/** * @FileName CircleProgressBar.java * @Package com.read.view * @Description TODO * @Author Alpha ...
- 【原创】C#玩高频数字彩快3的一点体会
购彩风险非常高,本人纯属很久以前对数字高频彩的一点研究.目前已经远离数字彩,重点研究足球篮球比赛资料库和赛果预测. 这是一篇在草稿箱保存了1年多的文章,一直没发现,顺便修改修改分享给大家.以后会有更多 ...
- HTML5的学习--performance获取加载时间的工具
前段时间因为项目需要获取页面加载的时间,就去看了下HTML5中的performane. 可以用其获得页面详细的加载时间. 关于performance的详细内容可以查看 http://www.cnblo ...
- Testing - 测试基础 - 探索
定义 探索性测试(Exploratory Testing)是一种自由的软件测试风格,强调测试人员同时展开测试学习,测试设计,测试执行和测试结果评估等活动,以持续优化测试工作. 其特征有:即兴发挥,快速 ...