经过了一段时间的泛函编程讨论,始终没能实实在在的明确到底泛函编程有什么区别和特点;我是指在现实编程的情况下所谓的泛函编程到底如何特别。我们已经习惯了传统的行令式编程(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除了名称怪异外实际上并不可怕,我们可以从它们的用途中了解它们的意义。