泛函编程(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

1   case class Box[A](a: A)
2   def map[A,B](f: A => B): Box[A] => Box[B] = {
3       (ba: Box[A]) => Box(f(ba.a))
4   }                                               //> map: [A, B](f: A => B)ch12.ex3.Box[A] => ch12.ex3.Box[B]

我们就试着用这个map来更新a:

1 //f: String => Int
2   def lengthOf(s: String): Int = s.length         //> lengthOf: (s: String)Int
3   val funcMapTransform = map(lengthOf)            //> funcMapTransform  : ch12.ex3.Box[String] => ch12.ex3.Box[Int] = <function1>
4   funcMapTransform(Box("Hello World!"))           //> res0: ch12.ex3.Box[Int] = Box(12)

恭喜!我们成功设计了个Functor函数

2、A => F[B]

1   case class Box[A](a: A)
2   def flatMap[A,B](f: A => Box[B]): Box[A] => Box[B] = {
3       (ba: Box[A]) => f(ba.a)
4   }

我们用flatMap进行Box[A] => Box[B]:

1 //f: String => Box[Int]
2   def boxedLengthOf(s: String) = Box(s.length)    //> boxedLengthOf: (s: String)ch12.ex3.Box[Int]
3   val funcFlatMapTransform = flatMap(boxedLengthOf)
4                                                   //> funcFlatMapTransform  : ch12.ex3.Box[String] => ch12.ex3.Box[Int] = <functio
5                                                   //| n1>
6   funcFlatMapTransform(Box("Hello World!"))       //> res1: ch12.ex3.Box[Int] = Box(12)

这个flatMap就是个Monad函数

3、Box[A => B]

1   case class Box[A](a: A)
2   def apply[A,B](f: Box[A => B]): Box[A] => Box[B] = {
3       (ba: Box[A]) => Box(f.a(ba.a))
4   }

我们可以使用一下这个apply函数:

1  //f: Box[String => Int]
2    val funcApplyTransform = apply(Box(lengthOf _))//> funcApplyTransform  : ch12.ex3.Box[String] => ch12.ex3.Box[Int] = <function1
3                                                   //| >
4    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,因为继承方式会更灵活:

1   trait Box[A] {
2       def get: A
3   }
4   object Box {
5       def apply[A](a: A) = new Box[A] {
6           def get = a
7       }
8   }

用get来获取嵌入结构的变量值。apply是Box类型的创造工厂函数。现在我们可以创建Box实例:

1   val bxHello = Box("Hello")                      //> bxHello  : ch12.ex4.Box[String] = ch12.ex4$Box$$anon$1@3581c5f3
2   bxHello.get                                     //> res0: String = Hello

如果Box是Functor,就必须实现map函数:

 1   trait Box[A] {
 2       def get: A
 3       def map[B](f: A => B): Box[B] = Box(f(get))
 4   }
 5   object Box {
 6       def apply[A](a: A) = new Box[A] {
 7           def get = a
 8       }
 9   }
10   val bxHello = Box("Hello")                      //> bxHello  : ch12.ex4.Box[String] = ch12.ex4$Box$$anon$1@3581c5f3
11   bxHello map {_.length}                          //> res0: ch12.ex4.Box[Int] = ch12.ex4$Box$$anon$1@340f438e
12   (bxHello map {a => a.length}).get               //> res1: Int = 5

现在Box是个Functor,bxHello是个Functor实例。

 1   trait Box[A] {
 2       def get: A
 3       def map[B](f: A => B): Box[B] = Box(f(get))
 4       def flatMap[B](f: A => Box[B]): Box[B] = f(get)
 5       def apply[B](f: Box[A => B]): Box[B] = Box(f.get(get))
 6   }
 7   object Box {
 8       def apply[A](a: A) = new Box[A] {
 9           def get = a
10       }
11   }
12   val bxHello = Box("Hello")                      //> bxHello  : ch12.ex4.Box[String] = ch12.ex4$Box$$anon$1@3581c5f3
13   bxHello map {_.length}                          //> res0: ch12.ex4.Box[Int] = ch12.ex4$Box$$anon$1@340f438e
14   (bxHello map {a => a.length}).get               //> res1: Int = 5
15   
16   bxHello flatMap {a => Box(a.length)}            //> res2: ch12.ex4.Box[Int] = ch12.ex4$Box$$anon$1@30c7da1e
17   (bxHello flatMap {a => Box(a.length)}).get      //> res3: Int = 5
18   
19   def lengthOf(s: String): Int = s.length         //> lengthOf: (s: String)Int
20   bxHello apply {Box(lengthOf _)}                 //> res4: ch12.ex4.Box[Int] = ch12.ex4$Box$$anon$1@5b464ce8
21   (bxHello apply {Box(lengthOf _)}).get           //> res5: Int = 5

实现了flatMap, apply后Box是Functor,Applicative同时还是Monad

值得关注的是Monad特性。有了Monad特性我们可以在for-comprehension这个封闭的环境里进行行令编程

1   val word = for {
2       x <- Box("Hello")
3       y = x.length
4       z <- Box(" World!")
5       w = x + z
6   } yield w                                       //> word  : ch12.ex4.Box[String] = ch12.ex4$Box$$anon$1@2d554825
7   word.get                                        //> res6: String = Hello World!

注意:在for-comprehension这个环境里,运算对象x,y,z,w都是脱了衣服的基础类型。这样我们才能采用熟悉的编程方式工作。

乘这个机会再示范另外一种实现方式:

 1  trait Box[A] {
 2       def get: A
 3   }
 4   object Box {
 5       def apply[A](a: A) = new Box[A] {
 6           def get = a
 7       }
 8   }
 9   class BoxOps[A](ba: Box[A]) {
10        def map[B](f: A => B): Box[B] = Box(f(ba.get))
11       def flatMap[B](f: A => Box[B]): Box[B] = f(ba.get)
12       def apply[B](f: Box[A => B]): Box[B] = Box(f.get(ba.get))
13   }
14   implicit def toBoxOps[A](ba: Box[A]) = new BoxOps(ba)
15                                                   //> toBoxOps: [A](ba: ch12.ex5.Box[A])ch12.ex5.BoxOps[A]
16   
17   val bxHello = Box("Hello")                      //> bxHello  : ch12.ex5.Box[String] = ch12.ex5$Box$$anon$1@511baa65
18   bxHello map {_.length}                          //> res0: ch12.ex5.Box[Int] = ch12.ex5$Box$$anon$1@340f438e
19   (bxHello map {a => a.length}).get               //> res1: Int = 5
20   
21   bxHello flatMap {a => Box(a.length)}            //> res2: ch12.ex5.Box[Int] = ch12.ex5$Box$$anon$1@30c7da1e
22   (bxHello flatMap {a => Box(a.length)}).get      //> res3: Int = 5
23   
24   def lengthOf(s: String): Int = s.length         //> lengthOf: (s: String)Int
25   bxHello apply {Box(lengthOf _)}                 //> res4: ch12.ex5.Box[Int] = ch12.ex5$Box$$anon$1@5b464ce8
26   (bxHello apply {Box(lengthOf _)}).get           //> res5: Int = 5
27   
28   val word = for {
29       x <- Box("Hello")
30       y = x.length
31       z <- Box(" World!")
32       w = x + z
33   } yield w                                       //> word  : ch12.ex5.Box[String] = ch12.ex5$Box$$anon$1@2d554825
34   word.get                                        //> res6: String = Hello World!

以上方式得到同样的数据类型效果。同时又能更好的对源代码进行分类组织,是规范的泛函组件库编码方式。

看来,Functor, Applicative, Monad除了名称怪异外实际上并不可怕,我们可以从它们的用途中了解它们的意义。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏MelonTeam专栏

Masonry源码阅读笔记——使用Block实现链式编程

导语 前段时间在阅读Masonry源码时,看到其内部使用了链式编程,比较有趣,这里简单分享一下; 在OC中实现链式编程并不难,最常用的实现是使用B...

1846
来自专栏XAI

Python入门教程之安装MyEclipse插件和安装Python环境

Python for MyEclipse 插件 Python for Windows 安装文件 安装好所需要的文件 。即可在MyEclipse里面开始Pytho...

48711
来自专栏Android知识点总结

Java总结IO之总集篇

字符流和字节流向来各行其事,很少有交集。 但Reader和Writer有两个奇子,名叫InputStreamReader(男)和OutputStreamWri...

1065
来自专栏mukekeheart的iOS之旅

No.014 Longest Common Prefix

14. Longest Common Prefix Total Accepted: 112204 Total Submissions: 385070 Diffi...

2176
来自专栏技术博客

编写高质量代码改善C#程序的157个建议[优先考虑泛型、避免在泛型中声明静态成员、为泛型参数设定约束]

  泛型并不是C#语言一开始就带有的特性,而是在FCL2.0之后实现的新功能。基于泛型,我们得以将类型参数化,以便更大范围地进行代码复用。同时,它减少了泛型类及...

502
来自专栏Netkiller

GSON 多层Map剥离

工作中遇到一个问题,我们提供给外包方的 json 无法Decode 。 一段简单 JSON 字符串,字符串如下。 String json= "{\"0\":{...

2724
来自专栏Android开发指南

16.语音识别

4269
来自专栏Java技术栈

跟我学 Java 8 新特性之 Stream 流(六)收集

我们前面的五篇文章基本都是在说将一个集合转成一个流,然后对流进行操作,其实这种操作是最多的,但有时候我们也是需要从流中收集起一些元素,并以集合的方式返回,我们把...

992
来自专栏ppjun专栏

Android十八章:设计模式SOLID五大原则

单一职责原则就是造成一个类改变的原因一个只有一个。再比如手机的电池是一个类,电池只为手机提供电源的职责。

762
来自专栏数据结构与算法

BZOJ3295: [Cqoi2011]动态逆序对(cdq分治)

对于序列A,它的逆序对数定义为满足i<j,且Ai>Aj的数对(i,j)的个数。给1到n的一个排列,按照某种顺序依次删

753

扫码关注云+社区