泛函编程(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 Immutability 和final的优点
Atitit Immutability 和final的优点 什么是 immutability? 其实细分起来有语法上的 immutable (例如 Java 里的 final 关键字), 和运行时对象 ...
- 每天一个linux命令(24):Linux文件类型与扩展名
Linux文件类型和Linux文件的文件名所代表的意义是两个不同的概念.我们通过一般应用程序而创建的比如file.txt.file.tar.gz ,这些文件虽然要用不同的程序来打开,但放在Linux文 ...
- 在Ubuntu搭建.NET Core环境
Ubuntu16.04配置.net core环境 Ubuntu 16.04 desktop下载地址:http://www.ubuntu.com/desktop 本次是用vmware安装该系统. ...
- MVC及WebAPI添加Jsonp支持
Windows Live Writer 有点问题,着色代码看起来不清晰,所以贴的图片,完整代码在最后. 1:MVC实现 大致思路就是实现一个JsonpResult,在ExecuteResult内实现支 ...
- 加载的过程中图片变形了? --教你自定义自动适配图片宽高比的RatioLayout
很多同行在开发中可能会遇到这样的问题,就是在加载图片的时候会出现图片变形的问题.其实这很可能就是你的图片宽高比和图片所在容器的宽高比不匹配造成的.比如说图片的宽为200,高为100.宽高比就是2,那么 ...
- 180分钟的python学习之旅
最近在很多地方都可以看到Python的身影,尤其在人工智能等科学领域,其丰富的科学计算等方面类库无比强大.很多身边的哥们也提到Python非常的简洁方便,比如用Django搭建一个见得网站只需要半天时 ...
- Introduction of Open CASCADE Foundation Classes
Open CASCADE Foundation Classes Open CASCADE基础类 eryar@163.com 一.简介 1. 基础类概述 Foundation Classes Overv ...
- nyoj 925 国王的烦恼(最小生成树)
/* 题意:N个城市中每两个城市有多条路径连接,可是因为路径存在的天数是有限的!以为某条路经不存在了 导致N个城市不能连通了,那么村名们就会抗议!问一共会有多少次抗议! 思路:最小生成树....我们用 ...
- Android基于mAppWidget实现手绘地图(六)–如何展示地图对象
为了展示选中的点,你需要完成以下步骤: 1.创建或者获得一个已经存在的图层 2.创建代表选中点的地图对象 3.把地图对象添加到图层 创建新图层 使用以下代码片段创建图层 int COFFEE_SHOP ...
- nodejs操作mongodb
一.下载地址 https://www.mongodb.com/download-center#community 二.控制台操作mongodb 1.安装完后添加环境变量. 2.在某个根目录下新建dat ...