函数编程中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如何将外部指定的普通函数应用到被上下文包裹的值函数 Somes 和 Nones中:

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

 

Applicative

当我们的值被一个上下文包裹,就像函子Functor:

之前我们讨论的是如何将一个普通函数应用到这个函子中,现在如果这个普通函数也是一个被上下文包裹的,怎么办?

在Haskell中,Control.Applicative 定义了 <*>, 它能知道如何应用一个被上下文包裹的函数到一个被上下文包裹的值中。

上图可见,Applicative内部也是将各自包裹的盒子打开,应用其中函数与值的计算,然后包裹新值在一个上下文中。

Just (+3) <*> Just 2 == Just 5

 

Swift并没有内建的Applicative. 可以显式打开包裹遍历实现::

extension Optional {
  func apply<U>(f: (T -> U)?) -> U? {
    switch f {
      case .Some(let someF): return self.map(someF)
      case .None: return .None
    }
  }
}

extension Array {
  func apply<U>(fs: [Element -> U]) -> [U] {
    var result = [U]()
      for f in fs {
        for element in self.map(f) {
          result.append(element)
        }
      }
      return result
    }
}

如果 self 和函数都是 .Some, 那么函数应用到解开包裹的optionm,否则返回 .None.

也可以使用定义 <*>来做同样事情:

infix operator <*> { associativity left }

func <*><T, U>(f: (T -> U)?, a: T?) -> U? {
  return a.apply(f)
}

func <*><T, U>(f: [T -> U], a: [T]) -> [U] {
  return a.apply(f)
}

 

i.e:

Optional.Some({ $0 + 3 }) <*> Optional.Some(2)
// => 5

使用 <*> 会有趣的事情发生:

> [(*2), (+3)] <*> [1, 2, 3]
[2, 4, 6, 4, 5, 6]

 

下面是使用Applicative能实现,而使用函子Functor不能实现的,你如何应用一个带有两个输入参数的函数到两个已经包裹的值中?

使用函子的代码如下:

> (+) <$> (Just 5)
Just (+5)
> Just (+5) <$> (Just 4)
ERROR ??? WHAT DOES THIS EVEN MEAN WHY IS THE FUNCTION WRAPPED IN A JUST

第二个发生错误了。

使用Applicative:

> (+) <$> (Just 5)
Just (+5)
> Just (+5) <*> (Just 3)
Just 8

Swift的代码如下:

func curriedAddition(a: Int)(b: Int) -> Int {
  return a + b
}

curriedAddition <^> Optional(2) <^> Optional(3)
// => COMPILER ERROR: Value of optional type '(Int -> Int)? not unwrapped; did you mean to use '!' or '??'

Applicative:

curriedAddition <^> Optional(2) <*> Optional(3)

ApplicativeFunctor 推到到一边. “只有大孩子才能使用带有任何数量参数的函数。”

func curriedTimes(a: Int)(b: Int) -> Int {
  return a * b
}

curriedTimes <^> Optional(5) <*> Optional(3)


Monad

 

函子funtor是将一个普通函数应用到包裹的值:

Applicative应用一个包裹的函数到一个包裹的值:

Monad 则是将一个会返回包裹值的函数应用到一个被包裹的值上,Haskell中使用“>>=”表示,而Swift使用“|”.

Haskell中的MayBe也是一个monad:

假设half是一个只工作于偶数数字的函数:

half x = if even x
then Just (x `div` 2)
else Nothing

Swift代码如下:

func half(a: Int) -> Int? {
  return a % 2 == 0 ? a / 2 : .None
}

对于这个half函数,其形象工作原理入下图,如果输入一个值,那么half会返回一个被包裹的值。

但是如果我们输入一个包裹的值,而不是普通的值呢?

这时原来的代码就不工作了,我们需要使用新的语法Haskell是“>>=”来将包裹后的值放入函数,“>>=”类似我们排堵的拔子:

代码工作如下:

> Just 3 >>= half
Nothing
> Just 4 >>= half
Just 2
> Nothing >>= half
Nothing

Swift的代码:

Optional(3) >>- half
// .None
Optional(4) >>- half
// 2
Optional.None >>- half
// .None

那么内部发生了什么?Monad 是另外一个typeclass. 这里是partial定义:

class Monad m where
(>>=) :: m a -> (a -> m b) -> m b

Swift代码:

// For Optional
func >>-<T, U>(a: T?, f: T -> U?) -> U?

// For Array
func >>-<T, U>(a: [T], f: T -> [U]) -> [U]

monad工作原理图如下:

首先获得一个Monad,如Just 3,其次定义一个返回Monad的函数如half,最后结果也会返回一个Monad。

Haskell中Maybe也是一个Monad:

instance Monad Maybe where
Nothing >>= func = Nothing
Just val >>= func = func val

Swift的Optional也是一个Monad。

下面是输入一个包裹值到一个函数中完整示意图:

第一步,绑定已经解除包裹的值,第二步,将已经解除包裹的值输入函数,第三步,一个被重新包裹的值被输出。

如果你输入无None,更加简单:

你可以像链条一样链接这些调用:

> Just 20 >>= half >>= half >>= half
Nothing

Swift代码:

Optional(20) >>- half >>- half >>- half
// => .None

 

总结

  1. 函子functor是一种实现fmap或map的数据类型
  2. applicative是一种实现了Applicative 或apply的数据类型
  3. monad是一种实现了Monad或flatmap的数据类型.
  4. Haskell的Maybe和Swift的Optional是functor函子 applicative和Monad。.

那么函子、applicative和Monad三个区别是什么?

  • functor: 应用一个函数到包裹的值,使用fmap/map.
  • applicative: 应用一个包裹的函数到包裹的值。
  • monad: 应用一个返回包裹值的函数到一个包裹的值。

 

本文另外一篇中文翻译:Functor, Applicative, 以及 Monad 的图片阐释

函数编程之道

什么是Monad

函数式编程