函数编程中functor和monad的形象解释
函数编程中Functor函子与Monad是比较难理解的概念,本文使用了形象的图片方式解释了这两个概念,容易理解与学习,分别使用Haskell和Swift两种语言为案例。
虽然Swift并不是一个函数式语言,但是我们可以用更多点代码来完成与Haskell同样的结果。Swift的代码见on GitHub.
这里是一个简单的值:
如果我们应用一个函数(+3)到这个值:
非常简答,是不是?现在我们拓展一下,上面的值我们没有设定上下文场景,如果我们假设一个值处于一种上下文场景context中,你可以将上下文场景看成是一个盒子,盒子里面放入的是一个值:
为什么要假设上下文场景呢?其实任何事物都无法脱离其与环境的关系,任何真理都是有上下文前提的,当然除了这条。
当值放入一个盒子上下文中时,如果你想将一个函数应用到这个值时,取决于上下文场景你会得到不同的结果。这个思想其实是Functors, Applicatives, Monads, Arrows等概念的基础。
那么上下文使用什么语法表达呢?再Haskell中使用Maybe,在Swift中使用Optional:
Swift:
enum Optional<T> {
case None
case Some(T)
}
Haskell:
data Maybe a = Nothing | Just a
Optional和Maybe类似两个盒子,其中包裹着两个不同的值函数:无(None)和有(Some)。
下面我们看看上面Swift中两个函数 .Some(T)
与 .None
.不同,也就是Haskell两个函数Nothing与Just a的不同。现在来谈谈函子。
Functor函子
当一个值被一个上下文包裹时,你不能使用普通函数应用到这个值:
这就是需要 map
的用途 (fmap
in Haskell), map
知道如何将普通函数应用到一个被上下文包裹的值中,比如,假设你要应用一个函数:+3,将这个函数应用到 .Some(2)
. 使用 map
:
func plusThree(addend: Int) -> Int {
return addend + 3
}
Optional.Some(2).map(plusThree)
// => .Some(5)
Haskell使用fmap,将+3函数应用到Just 2:
> fmap (+3) (Just 2)
Just 5
fmap告诉我们它已经成功完成,但是它是如何应用函数到盒子里的值呢?
函子到底是什么?
一个函子Functor是任意类型,这些类型定义了如何应用 map
(fmap
in Haskell) 。 也就是说,如果我们要将普通函数应用到一个有盒子上下文包裹的值,那么我们首先需要定义一个叫Functor的数据类型,在这个数据类型中需要定义如何使用map或fmap来应用这个普通函数。这个函子Functor如下图:
fmap的输入参数是a->b函数,在我们这个案例中是(+3),然后定义一个函子Functor,这里是Haskell的Just 2,最后返回一个新的函子,在我们案例中,使用Haskell是Just 5。
我们再看看在Swift中如何实现,下面是autoclosure实现的函子:
Optional.Some(2).map { $0 + 3 }
// => .Some(5)
这里 map
魔术地使用了我们的函数(+3),这是因为 Optional
是一个函子,它定义了map如何将外部指定的普通函数应用到被上下文包裹的值函数 Some
s 和 None
s中:
func map<U>(f: T -> U) -> U? {
swifth self {
case .Some(let x): return f(x)
case .None: return .None
}
下图展示了函子内部工作原理:
第一步是将值从上下文盒子中解救出来,然后将外部指定的函数(+3)应用到这个值上,得到一个新的值(5),再将这个新值放入到上下文盒子中。是不是很形象生动?
当然,你也可以将(+3)应用到None上:
Optional.None.map { $0 + 3 }
// => .None
map或fmap知道如果你应用函数到None,你同样得到的还是无None,这里map或fmap是不是像禅一样呢?
现在我们知道Optional类型或Maybe类型存在的原因了吧?我们再用实际中案例说明,如果我们使用一种没有Optional/Maybe类型的语言从数据库查询结果,比如Ruby:
let post = Post.findByID(1)
if post != nil {
return post.title
} else {
return nil
}
而使用有Optional/Maybe类型的语言Swift/Haskell,则可以使用函子Optional:
findPost(1).map(getPostTitle)
如果 findPost(1)
返回一个帖子, 我们会使用 getPostTitle
函数得到这个帖子的标题,如果返回无 None
, 我们也返回无None
!
我们可以甚至定义一个中缀操作符号,Swift的map的中缀操作符是<^>,而Haskell的fmap是<$>。:
Haskell直接写为:
getPostTitle <$> (findPost 1)
Swift代码如下:
infix operator <^> { associativity left }
func <^><T, U>(f: T -> U, a: T?) -> U? {
return a.map(f)
}
getPostTitle <^> findPost(1)
下图显示了如何将一个普通函数应用到值集合,不是单个值,而是值的集合数组中:
图中数组函子将数组一个个打开(遍历),然后分别将普通函数应用到这些元素中,最后返回一个新的集合值。Haskell中定义:
instance Functor [] where fmap = map
下面我们看看将一个函数应用到另外一个函数的情况:
fmap (+3) (+1)
这里我们将函数(+3)应用到(+1)函数上,首先我们看看什么是函数,见下图:
一个函数是输入一个值,返回一个值。
下图是将一个函数应用到另外一个函数:
结果就是另外一个函数!
> import Control.Applicative
> let foo = fmap (+3) (+2)
> foo 10
15
所以说,函数也是一个函子functor:
instance Functor ((->) r) where fmap f g = f . g
你在一个函数上使用fmap ,实际你在做函数的组合,如同堆积木一样。
Swift将一个函数应用到另外一个函数的代码如下:
typealias IntFunction = Int -> Int
func map(f: IntFunction, _ g: IntFunction) -> IntFunction {
return { x in f(g(x)) }
}
let foo = map({ $0 + 2 }, { $0 + 3 })
foo(10)
// => 15
本文另外一篇中文翻译:Functor, Applicative, 以及 Monad 的图片阐释
函数编程之道