泛函编程(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 条评论
登录 后参与评论

相关文章

来自专栏debugeeker的专栏

《coredump问题原理探究》windows版7.6节string

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/xuzhina/article/detai...

722
来自专栏吉浦迅科技

DAY36:阅读”执行空间&扩展修饰符

1113
来自专栏青枫的专栏

4道html笔试小题

725
来自专栏云时之间

深度学习与神经网络:制作数据集,完成应用(1)

1434
来自专栏小樱的经验随笔

洛谷 P1914 小书童——密码【字符串+模拟】

P1914 小书童——密码 题目背景 某蒟蒻迷上了“小书童”,有一天登陆时忘记密码了(他没绑定邮箱or手机),于是便把问题抛给了神犇你。 题目描述 蒟蒻虽然忘记...

2697
来自专栏机器之心

入门 | 简易指南带你启动 R 语言学习之旅

2034
来自专栏hightopo

原 荐 WebGL 3D 电信机架实战之数据

1466
来自专栏漫漫深度学习路

tensorflow:AToolDeveloperGuideToTFModelFIles

Tensorflow Model Files 最近闲来无聊,想深入理解一下tensorlfow,也不知从何下手,突然间发现了官方文档的Extend模块下还有这个...

2625
来自专栏十月梦想

js实现随求抓取样本数据(批量或者样本元素)

马上期末汇报学期项目了,这个居然要随机点名汇报,突然想起是否可以使用筛选数据,批量抽取样本中数据进行排序!

672
来自专栏C/C++基础

小米Git

题目描述: git是一种分布式代码管理工具,git通过树的形式记录文件的更改历史,比如: base’<–base<–A<–A’ ^ | — B<–B’ 小米...

381

扫码关注云+社区