泛函编程(28)-粗俗浅解:Functor, Applicative, Monad
经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点;我是指在现实编程的情况下所谓的泛函编程到底如何特别。我们已经习惯了传统的行令式编程(imperative programming),总是先入为主的认为软件编程就是一行接着一行的更改某些变量状态指令:明刀明枪,字里行间目的和方式都很明确。我们就以一步步更改程序状态的方式,一行一行的拼凑指令:这就是典型的行令式编程了。
泛函编程,顾名思义,就是用一个个函数来编程。讲的再深入点就是通过函数组合来更改程序状态。什么意思?为什么?
严格来讲,在泛函编程中是没有在某个地方申明一个变量,然后在一些函数里更新这个变量这种方式的。与申明变量相对应的是泛函编程会把所谓变量嵌入在一个结构里,如:F[A]。F是某种高阶类型,A就是那个变量。如果我们需要去更改这个变量A就必须设计一套专门的函数来做这件事了。从某些方面这也解释了何谓泛函编程。我用粗俗的语言来描述这两种编程模式的区别:行令编程就像在床面上打扑克,而泛函编程就好比在被窝里打牌。两种操作一样都是打牌,只是打牌的环境不同。实际上泛函编程的这种在套子内部更新变量的方式恰恰是我们选择泛函模式的考虑重点:它可以使程序运行更安全稳定、能轻松解决很多行令编程模式中存在的难题,这些优点将会在将来的应用中逐渐显现出来。
既然变量封装在了套子里面,那么自然需要设计一些在套子里更新变量的函数了:
我们的目的是用某些函数把F[A]变成F[B]:A 变成了 B,但任然封装在 F[] 里:
下面我们列出几个函数,它们的操作结果都是一样的:
A => B >>> F[A] => F[B]
A => F[B] >>> F[A] => F[B]
F[A => B] >>> F[A] => F[B]
就是说我们有这三款函数,问题是应该怎么把F[A]变成F[B]。我们先定义一个测试用的数据类型:
case class Box[A](a: A) >>> 这是一个带嵌入变量的泛函类型
下面我们就试着实现这三款函数:
1、 A => B
case class Box[A](a: A)
def map[A,B](f: A => B): Box[A] => Box[B] = {
(ba: Box[A]) => Box(f(ba.a))
} //> map: [A, B](f: A => B)ch12.ex3.Box[A] => ch12.ex3.Box[B]
我们就试着用这个map来更新a:
//f: String => Int
def lengthOf(s: String): Int = s.length //> lengthOf: (s: String)Int
val funcMapTransform = map(lengthOf) //> funcMapTransform : ch12.ex3.Box[String] => ch12.ex3.Box[Int] = <function1>
funcMapTransform(Box("Hello World!")) //> res0: ch12.ex3.Box[Int] = Box(12)
恭喜!我们成功设计了个Functor函数
2、A => F[B]
case class Box[A](a: A)
def flatMap[A,B](f: A => Box[B]): Box[A] => Box[B] = {
(ba: Box[A]) => f(ba.a)
}
我们用flatMap进行Box[A] => Box[B]:
//f: String => Box[Int]
def boxedLengthOf(s: String) = Box(s.length) //> boxedLengthOf: (s: String)ch12.ex3.Box[Int]
val funcFlatMapTransform = flatMap(boxedLengthOf)
//> funcFlatMapTransform : ch12.ex3.Box[String] => ch12.ex3.Box[Int] = <functio
//| n1>
funcFlatMapTransform(Box("Hello World!")) //> res1: ch12.ex3.Box[Int] = Box(12)
这个flatMap就是个Monad函数
3、Box[A => B]
case class Box[A](a: A)
def apply[A,B](f: Box[A => B]): Box[A] => Box[B] = {
(ba: Box[A]) => Box(f.a(ba.a))
}
我们可以使用一下这个apply函数:
//f: Box[String => Int]
val funcApplyTransform = apply(Box(lengthOf _))//> funcApplyTransform : ch12.ex3.Box[String] => ch12.ex3.Box[Int] = <function1
//| >
funcApplyTransform(Box("Hello World!")) //> res2: ch12.ex3.Box[Int] = Box(12)
apply函数就是Applicative函数
虽然我们在上面分别实现了Functor,Applicative,Monad的功能函数。但Functor,Applicative,Monad都是泛函数据类型,我们还没有明确定义这些数据类型。这些数据类型自提供了操作函数对嵌在内部的变量进行更新。也就是说它们应该自带操作函数。
我们再来看看以上的函数款式:
(A => B) => (F[A] => F[B])
(A => B) => F[A] => F[B]
uncurry ((A => B), F[A]) => F[B]
map(F[A])(A => B): F[B]
flatMap(F[A])(A => F[B]): F[B]
apply(F[A])(F[A => B]): F[B]
现在所有函数都针对共同的数据类型F[A]。它们已经具备了数据类型内嵌函数的特性。
下面我们再用规范方式定义F[A]这个数据类型。我们采用trait,因为继承方式会更灵活:
trait Box[A] {
def get: A
}
object Box {
def apply[A](a: A) = new Box[A] {
def get = a
}
}
用get来获取嵌入结构的变量值。apply是Box类型的创造工厂函数。现在我们可以创建Box实例:
val bxHello = Box("Hello") //> bxHello : ch12.ex4.Box[String] = ch12.ex4$Box$$anon$1@3581c5f3
bxHello.get //> res0: String = Hello
如果Box是Functor,就必须实现map函数:
trait Box[A] {
def get: A
def map[B](f: A => B): Box[B] = Box(f(get))
}
object Box {
def apply[A](a: A) = new Box[A] {
def get = a
}
}
val bxHello = Box("Hello") //> bxHello : ch12.ex4.Box[String] = ch12.ex4$Box$$anon$1@3581c5f3
bxHello map {_.length} //> res0: ch12.ex4.Box[Int] = ch12.ex4$Box$$anon$1@340f438e
(bxHello map {a => a.length}).get //> res1: Int = 5
现在Box是个Functor,bxHello是个Functor实例。
trait Box[A] {
def get: A
def map[B](f: A => B): Box[B] = Box(f(get))
def flatMap[B](f: A => Box[B]): Box[B] = f(get)
def apply[B](f: Box[A => B]): Box[B] = Box(f.get(get))
}
object Box {
def apply[A](a: A) = new Box[A] {
def get = a
}
}
val bxHello = Box("Hello") //> bxHello : ch12.ex4.Box[String] = ch12.ex4$Box$$anon$1@3581c5f3
bxHello map {_.length} //> res0: ch12.ex4.Box[Int] = ch12.ex4$Box$$anon$1@340f438e
(bxHello map {a => a.length}).get //> res1: Int = 5
bxHello flatMap {a => Box(a.length)} //> res2: ch12.ex4.Box[Int] = ch12.ex4$Box$$anon$1@30c7da1e
(bxHello flatMap {a => Box(a.length)}).get //> res3: Int = 5
def lengthOf(s: String): Int = s.length //> lengthOf: (s: String)Int
bxHello apply {Box(lengthOf _)} //> res4: ch12.ex4.Box[Int] = ch12.ex4$Box$$anon$1@5b464ce8
(bxHello apply {Box(lengthOf _)}).get //> res5: Int = 5
实现了flatMap, apply后Box是Functor,Applicative同时还是Monad
值得关注的是Monad特性。有了Monad特性我们可以在for-comprehension这个封闭的环境里进行行令编程
val word = for {
x <- Box("Hello")
y = x.length
z <- Box(" World!")
w = x + z
} yield w //> word : ch12.ex4.Box[String] = ch12.ex4$Box$$anon$1@2d554825
word.get //> res6: String = Hello World!
注意:在for-comprehension这个环境里,运算对象x,y,z,w都是脱了衣服的基础类型。这样我们才能采用熟悉的编程方式工作。
乘这个机会再示范另外一种实现方式:
trait Box[A] {
def get: A
}
object Box {
def apply[A](a: A) = new Box[A] {
def get = a
}
}
class BoxOps[A](ba: Box[A]) {
def map[B](f: A => B): Box[B] = Box(f(ba.get))
def flatMap[B](f: A => Box[B]): Box[B] = f(ba.get)
def apply[B](f: Box[A => B]): Box[B] = Box(f.get(ba.get))
}
implicit def toBoxOps[A](ba: Box[A]) = new BoxOps(ba)
//> toBoxOps: [A](ba: ch12.ex5.Box[A])ch12.ex5.BoxOps[A]
val bxHello = Box("Hello") //> bxHello : ch12.ex5.Box[String] = ch12.ex5$Box$$anon$1@511baa65
bxHello map {_.length} //> res0: ch12.ex5.Box[Int] = ch12.ex5$Box$$anon$1@340f438e
(bxHello map {a => a.length}).get //> res1: Int = 5
bxHello flatMap {a => Box(a.length)} //> res2: ch12.ex5.Box[Int] = ch12.ex5$Box$$anon$1@30c7da1e
(bxHello flatMap {a => Box(a.length)}).get //> res3: Int = 5
def lengthOf(s: String): Int = s.length //> lengthOf: (s: String)Int
bxHello apply {Box(lengthOf _)} //> res4: ch12.ex5.Box[Int] = ch12.ex5$Box$$anon$1@5b464ce8
(bxHello apply {Box(lengthOf _)}).get //> res5: Int = 5
val word = for {
x <- Box("Hello")
y = x.length
z <- Box(" World!")
w = x + z
} yield w //> word : ch12.ex5.Box[String] = ch12.ex5$Box$$anon$1@2d554825
word.get //> res6: String = Hello World!
以上方式得到同样的数据类型效果。同时又能更好的对源代码进行分类组织,是规范的泛函组件库编码方式。
看来,Functor, Applicative, Monad除了名称怪异外实际上并不可怕,我们可以从它们的用途中了解它们的意义。
泛函编程(28)-粗俗浅解:Functor, Applicative, Monad的更多相关文章
- 泛函编程(27)-泛函编程模式-Monad Transformer
经过了一段时间的学习,我们了解了一系列泛函数据类型.我们知道,在所有编程语言中,数据类型是支持软件编程的基础.同样,泛函数据类型Foldable,Monoid,Functor,Applicative, ...
- 泛函编程(6)-数据结构-List基础
List是一种最普通的泛函数据结构,比较直观,有良好的示范基础.List就像一个管子,里面可以装载一长条任何类型的东西.如需要对管子里的东西进行处理,则必须在管子内按直线顺序一个一个的来,这符合泛函编 ...
- 泛函编程(26)-泛函数据类型-Monad-Applicative Functor Traversal
前面我们讨论了Applicative.Applicative 就是某种Functor,因为我们可以用map2来实现map,所以Applicative可以map,就是Functor,叫做Applicat ...
- 泛函编程(25)-泛函数据类型-Monad-Applicative
上两期我们讨论了Monad.我们说Monad是个最有概括性(抽象性)的泛函数据类型,它可以覆盖绝大多数数据类型.任何数据类型只要能实现flatMap+unit这组Monad最基本组件函数就可以变成Mo ...
- Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理
Java网络编程和NIO详解7:浅谈 Linux 中NIO Selector 的实现原理 转自:https://www.jianshu.com/p/2b71ea919d49 本系列文章首发于我的个人博 ...
- 浅释Functor、Applicative与Monad
引言 转入Scala一段时间以来,理解Functor.Applicative和Monad等概念,一直是我感到头疼的部分.虽然读过<Functors, Applicatives, And Mona ...
- Monad / Functor / Applicative 浅析
前言 Swift 其实比 Objective-C 复杂很多,相对于出生于上世纪 80 年代的 Objective-C 来说,Swift 融入了大量新特性.这也使得我们学习掌握这门语言变得相对来说更加困 ...
- 泛函编程(32)-泛函IO:IO Monad
由于泛函编程非常重视函数组合(function composition),任何带有副作用(side effect)的函数都无法实现函数组合,所以必须把包含外界影响(effectful)副作用不纯代码( ...
- 泛函编程(24)-泛函数据类型-Monad, monadic programming
在上一节我们介绍了Monad.我们知道Monad是一个高度概括的抽象模型.好像创造Monad的目的是为了抽取各种数据类型的共性组件函数汇集成一套组件库从而避免重复编码.这些能对什么是Monad提供一个 ...
随机推荐
- Atitit 数据存储的数据表连接attilax总结
Atitit 数据存储的数据表连接attilax总结 1.1. 三种物理连接运算符:嵌套循环连接.合并连接以及哈希连接1 1.2. a.嵌套循环连接(nested loops join)1 1.3. ...
- WPF入门教程系列六——布局介绍与Canvas(一)
从这篇文章开始是对WPF中的界面如何布局做一个较简单的介绍,大家都知道:UI是做好一个软件很重要的因素,如果没有一个漂亮的UI,功能做的再好也无法吸引很多用户使用,而且没有漂亮的界面,那么普通用户会感 ...
- Java 集合系列目录(Category)
下面是最近总结的Java集合(JDK1.6.0_45)相关文章的目录. 01. Java 集合系列01之 总体框架 02. Java 集合系列02之 Collection架构 03. Java 集合系 ...
- Unity 游戏存档 PlayerPrefs类的用法
unity3d提供了一个用于本地持久化保存与读取的类--PlayerPrefs.工作原理非常简单,以键值对的形式将数据保存在文件中,然后程序可以根据这个名称取出上次保存的数值. PlayerPr ...
- Nodejs学习笔记(三)——一张图看懂Nodejs建站
前言:一条线,竖着放,如果做不到精进至深,那就旋转90°,至少也图个幅度宽广. 通俗解释上面的胡言乱语:还没学会爬,就学起走了?! 继上篇<Nodejs学习笔记(二)——Eclipse中运行调试 ...
- 判断js数据类型和clone
判断返回js数据类型 function judgeType(arg){//判断返回js数据类型 return Object.prototype.toString.call(arg).slice(8,- ...
- Database Primary key and Foreign key [From Internet]
Database Primary key and Foreign key --Create Referenced Table CREATE TABLE Department ( DeptID int ...
- Elasticsearch聚合 之 Date Histogram聚合
Elasticsearch的聚合主要分成两大类:metric和bucket,2.0中新增了pipeline还没有研究.本篇还是来介绍Bucket聚合中的常用聚合--date histogram.参考: ...
- Android Studio添加PNG图片报错原因
今天在网上看到一个关于Splash Activity的Android帖子,博主在一通讲解之后也给出了代码.于是果断下载下来了看看怎么实现的.一步步照着流程把这个功能实现了一遍.一切都没有大问题,但是在 ...
- Fiddler使用AutoResponder进行本地文件和线上文件的映射
直入正题.. 刚刚想写篇总结,照常登上博客园添加新随笔,写了百来字发现有什么不对劲 编辑器去哪儿了... 心想重新登录应该就OK了吧,点击退出然后重新登录 按钮一直显示加载中...简直就没法登录嘛.. ...