理解函数式编程中的函数组合--Monoids(二)

时间:2022-01-18 03:47:58

使用函数式语言来建立领域模型--类型组合

理解函数式编程语言中的组合--前言(一)

理解函数式编程中的函数组合--Monoids(二)

继上篇文章引出《范畴论》之后,我准备通过几篇文章,来介绍函数式编程语言中的若干"行话",例如Functor, Applicative, Monad。如果给这些名字一个通俗的名称,我觉得Combinator(组合子)比较形象一些,组合子可以将函数组合起来。我在一篇文章中还看到过一个另一个通俗的说法--“胶水函数”,简而言之就是如果两个函数与不能够直接组合,那么就可以通过一种像胶水一样的函数,把两个函数粘接起来,从而达到组合函数的目的。

在正式讲解这些概念之前,我想提一下“行话”这一现象,其实不光是函数式编程领域,OO设计里也有不少“行话”或者说“术语“,例如,”依赖注入“, ”多态“, ”桥接模式“,这些词大家听着都不陌生,源于大家对OO的长期实践。但是如果摒弃偏见,理解并灵活应用这些概念并不是一蹴而就的。有时候你觉得简单,只是因为更熟悉而已。

这篇文章为大家介绍《范畴论》里的一个基础概念-Monoids(注意,不是Monad)。另外本文的例子都通过TypeScript来描述,另外本文的术语都会保持英文名称,因为这些术语翻译为汉语价值不大,另外保持英文名称也方便大家搜索相关介绍。

Monoids

首先Monoids一词来源于数学家,翻译成中文没有任何意义,你不会从中文翻译里面得到任何关于Monoids含义的线索,如果非要给他一个中文翻译,我会翻译为”可聚合的事物"。当你理解了Monoids, 你就会发现在生活中,处处存在着Monoids。 只不过数学家善于归纳总结,给与了这一类事物一个确切的定义和相应的定律。

让我们还原一下数学家发现这类事物的场景:

可聚合的事物

1 + 2 = 3

这行数学运算可以描述为:两个“数字”通过“相加”运算,得到了一个结果,其结果任然为“数字”。

"a" + "b" = "ab"

上面这行运算可以描述为:两个"字符“通过”拼接“操作,得到了一个结果,其结果任然为”字符串“。

如果我们将上面的这两个运算进一步泛化,就会得到类下面的模式(pattern):

  • 有两个事物
  • 两个事物能够通过一种组合方式将他们组合起来
  • 得到的事物跟之前的类型是一致的

这个规律能够运用在非数字或者字符串之外的其他事物上面吗?假如这种事物可以通过某种方式组合到一起,是不是就能够符合这一规律呢?

钱算不算?

type Money = {
amount: number
}; const a: Money = { amount: 10.2 }
const b: Money = { amount: 2.8 }
const c: Money = { amount: a.amount + b.amount }

你如果熟悉DDD中提到的ValueObject,你可以将这模式应用在很多事物(ValueObject)上。

为什么这个模式要强调组合之后的事物跟之前的类型是一致的(closure)?

因为你可以把这个模式推广到list上

换句话说,如果一个二元运算如果返回的类型跟之前一致,就可以把这个操作符应用到一个list上,这个函数叫做reduce。

[0, 2, 3, 4].reduce((acc, val) => acc + val);
["a", "b", "c", "d"].reduce((acc, val) => acc + val);

MapReduce大家应该都不陌生,为什么叫Map? 因为需要将数据转化为Monoids, 为什么要Reduce? 因为需要聚合数据。

结合律

实际上,只符合上面的模式,还不能称之为为Monoids, 确切的说叫做Magma。我们小学数学都学习过结合律(Associative),注意不是交换律(commutative),例如:

(1 + 2) + 3 = 1 + (2 + 3) = 6

结合律说左右组合顺序不重要,得到的结果都是一样的,这一定律实际上对事物组合的运算符做出了限制,例如,针对数字运算,乘法符合结合律吗?

(1 * 2) * 3 = 1 * (2 * 3) = 6

答案是符合,那么除法和减法呢?

(1 - 2) - 3 != 1 - (2 - 3)
(1 % 2) % 3 != 1 % (2 % 3)

除法和减法不符合结合律,为什么结合律这么重要?

因为当顺序不是问题的时候,并行计算和累加就显得轻而易举。因为执行顺序不是问题,你就可以把计算量分配到若干个机器上,然后累加结果。或者说今天计算了任务的30%,等明天启动任务的时候接着计算,而不需要重新计算整个数据集。

Identity元素

目前为止,得到的事物叫Semigroups,只差最后一个条件便可称之为Monoids。看下面的运算:

1 + 0 = 1
"a" + "" = "a"

有什么规律呢?针对数字和”加法“运算,任何数字加0,得到的结果跟之前一样。针对字符串和”加法“运算,任何字符串和”空字符串“拼接起来,得到的结果也跟之前一样。

对于数字和”乘法“运算来说,0元素是1:

10 * 1 = 10

对于list而言,0元素是空list:

const a = [1, 2, 3]
const b: number[] = []
const c = [...a, ...b] expect(c).toEqual(a);

数学家把这个类似于0一样的元素称之为identity元素,为什么需要identity元素呢?

试想一下如何对一个空数组做reduce?

const a: number[] = [];
const result = a.reduce((acc, val) => acc + val);

这行代码会报错,reduce函数会抱怨你没有提供一个初始值,而这个不影响计算结果的初始值,实际就是identity元素。

const result = a.reduce((acc, val) => acc + val, 0);

大部分语言把提供初始值的函数称之为fold函数。不过fold的基础并不是Monoids, 而是Catamorphisms,在此不再细说。

Monoids定律

数学家将上面的三个规律定义为三个定律(laws):

  • 定律1 (Closure): 两个事物合并后总能得到相同类型的事物。
  • 定律2 (Associativity): 当组合一组事物时,组合的顺序不会影响结果(不是交换律的那种顺序)。
  • 定律3 (Identity element): 有一个0元素,任何事物跟0元素合并之后的结果不变。

    用数学家的话说,凡是符合上面三个定律的事物被称之为Monoids。符合定律1的叫做Magma, 同时符合定律1和定律2的称之为Semigroups。

    理解函数式编程中的函数组合--Monoids(二)

用一句话概括,Monoids是一个能够满足结合律,拥有Identity元素的二元运算。如果用代码来定义,大概如下:

interface Monoid<A> {
readonly concat: (x: A, y: A) => A
readonly empty: A
}

结合律则要满足下面的等式:

concat(x, concat(y, z)) = concat(concat(x, y), z)

上面用来描述Monoids的方式,在函数式编程语言里叫做type classes。严格来说,TypeScript原生并不支持type classes,也不支持Higher Kinded Types(HKT), 上面的例子只是我们用interface来模拟了一个type classes定义。

对于原生支持type classes的语言,例如Haskell, Monoid被定义为:

class Monoid m where
mempty :: m
mappend :: m -> m -> m
mconcat :: [m] -> m
mconcat = foldr mappend mempty

让我们对这个定义做个简单分析,首先,m这种类型可以作为Monoid实例,只要符合:

  • mempty代表Identty 元素
  • mappend代表一个函数,接受两个相同类型的参数,然后返回一个类型也一样的值,可以理解为二元操作符
  • mconcat是一个函数,接受一组monoid值,然后聚合为一个值。它拥有一个默认实现,使用mappend操作符和mempty作为默认值,来fold一个列表

可以看出Haskell里面的的type class基本跟我们在TypeScript里用interfaced定义出来的type class差不多,实际上是不是原生支持Type classes,并不影响TypeScript可以作为一门函数式编程语言,类似的语言还有F#等。

函数也可以是Monoids

大家要明白《范畴论》的抽象程度很高,Monoid并不单单指我们在文章中提到的字符串,数字之类,它可以是宇宙中的任何符合Monoids law的事物,这个事物也可以是函数。在TypeScript里,定义一个具有一个参数和返回类型的函数如下:

type func = <T1, T2>(x: T1) => T2

这个函数的签名如下:

T1 -> T2

在一个函数a->b中,如果b是monoid,那么这个函数也是一个monoid。也就是说函数签名相同的两个函数是可以组合的。相关过程我不再证明,在Haskell里,这样的一条规则可以被描述为:

instance Monoid b => Monoid (a -> b)

特别的,当函数是一个monoid并且其输入类型和输出类型一致时,被称为Endomorphism monoid。

type func = <T>(x: T) => T

Monoid实战

如果说“数字”再加上"加法"操作符就是Monoid, 那么通过reduce就可以轻而易举的将一堆数字累加起来。让我们看一个稍微复杂的例子,例如在购物车里,每个商品都可以用下面的类型来表示:

type OrderLine = {
id: number,
name: string,
quality: number,
total: number
}

用命令式的思想来汇总总价,通常就是一个for循环,然后累加结果。不过,你应该想到,Monoid就是用来解决数据的累加问题,我们可以通过reduce解决问题,你可能会想到这样做:

const total = orderLines.reduce((acc, val) => acc.total + val.total)

这行代码会报错,编译器会抱怨你在reduce函数里传入的高阶函数签名不符合要求,因为你传入的函数签名如下:

OrderLine -> OrderLine -> number

这个函数不符合Monoid定律,即返回类型不是一个OrderLine类型。Reduce期望你传入的函数类型签名为:

OrderLine -> OrderLine -> OrderLine

我们只需要将这个高阶函数的返回类型也定义为OrderLine即可,即:

const addTwoOrderLines = (line1: OrderLine, line2: OrderLine): OrderLine => (
{
name: "total",
quality: line1.quality + line2.quality,
total: line1.total + line2.total
}
)
const total = orderLines.reduce(addTwoOrderLines)

结束语

本文通过描述Monoid带大家进入函数式编程和《范畴论》的世界,为了进一步用代码实现这些例子,我在接下来的文章中还会引入fp-ts,从而通过TypeScript来展示一些实例。

理解函数式编程中的函数组合--Monoids(二)的更多相关文章

  1. 从函数式编程到Ramda函数库(二)

    Ramda 基本的数据结构都是原生 JavaScript 对象,我们常用的集合是 JavaScript 的数组.Ramda 还保留了许多其他原生 JavaScript 特性,例如,函数是具有属性的对象 ...

  2. Python函数式编程中map&lpar;&rpar;、reduce&lpar;&rpar;和filter&lpar;&rpar;函数的用法

    Python中map().reduce()和filter()三个函数均是应用于序列的内置函数,分别对序列进行遍历.递归计算以及过滤操作.这三个内置函数在实际使用过程中常常和“行内函数”lambda函数 ...

  3. 跟着ALEX 学python day3集合 文件操作 函数和函数式编程 内置函数

    声明 : 文档内容学习于 http://www.cnblogs.com/xiaozhiqi/  一. 集合 集合是一个无序的,不重复的数据组合,主要作用如下 1.去重 把一个列表变成集合 ,就自动去重 ...

  4. C&num;函数式编程之由函数构建函数

    在面向对象的编程中,如果我们需要复用其他的类,我们可以通过继承来实现.而在函数式编程中我们也可以采取不同的方式来复用这些函数.今天的教程将会讲述两种方式,其中一个就是组合,将多个函数组合成为一个函数, ...

  5. 从函数式编程到Ramda函数库(一)

    函数式编程是种编程方式,它将电脑运算视为函数的计算.函数编程语言最重要的基础是λ演算(lambda calculus),而且λ演算的函数可以接受函数当作输入(参数)和输出(返回值).和指令式编程相比, ...

  6. python函数式编程之返回函数、匿名函数、装饰器、偏函数学习

    python函数式编程之返回函数 高阶函数处理可以接受函数作为参数外,还可以把函数作为结果值返回. 函数作为返回值 def laxy_sum(*args): def sum(): ax = 0; fo ...

  7. js函数式编程&lpar;一&rpar;-纯函数

    我将写的第一个主题是js的函数式编程,这一系列都是mostly adequate guide这本书的读书总结.原书在gitbook上,有中文版.由于原作者性格活泼,书中夹杂很多俚语,并且行文洒脱.中文 ...

  8. JavaScript闭包其一:闭包概论 函数式编程中一些基本定义

    http://www.nowamagic.net/librarys/veda/detail/1707前面介绍了作用域链和变量对象,现在再讲闭包就容易理解了.闭包其实大家都已经谈烂了.尽管如此,这里还是 ...

  9. 【Python】&lbrack;函数式编程&rsqb;高阶函数,返回函数,装饰器,偏函数

    函数式编程高阶函数 就是把函数作为参数的函数,这种抽象的编程方式就是函数式编程.--- - -跳过,不是很理解,汗 - ---

随机推荐

  1. nginx和tomcat实现反向代理、负载均衡和session共享

    这类的文章很多,nginx和tomcat实现反向代理.负载均衡实现很容易,可以参照http://blog.csdn.net/liuzhigang1237/article/details/8880752 ...

  2. pymongo使用总结

    0. 何为pymongo pymongo是操作MongoDB的python模块 1.安装pymongo # easy_install pymongo 2.连接mongodb >>> ...

  3. Can&&num;39&semi;t create&sol;write to file &&num;39&semi;&sol;tmp&sol;&num;sql&lowbar;887d&lowbar;0&period;MYD&&num;39&semi; &lpar;Errcode&colon; 17&rpar;

    lsof |grep "#sql_887d_0.MYD" 如果没有被占用就可以删掉 . https://wordpress.org/support/topic/cant-creat ...

  4. JAVA解决大数

    主题链接:CLICK HERE~ 有了Java求解大数变得如此简单,以后再也不用操心大数模板了.哦啦啦啦. import java.math.BigInteger; import java.math. ...

  5. hdu 5600 BestCoder Round &num;67 &lpar;div&period;2&rpar;

    N bulbs  Accepts: 275  Submissions: 1237  Time Limit: 10000/5000 MS (Java/Others)  Memory Limit: 655 ...

  6. 数据结构 栈&amp&semi;队列

    2-4 依次在初始为空的队列中插入元素a,b,c,d以后,紧接着做了两次删除操作,此时的队头元素是( ) 删除,移动头指针: 增加,移动尾指针: 删除a,b ,队头c 2-3 在一个链队列中,fron ...

  7. java多线程系列六、线程池

    一. 线程池简介 1. 线程池的概念: 线程池就是首先创建一些线程,它们的集合称为线程池. 2. 使用线程池的好处 a) 降低资源的消耗.使用线程池不用频繁的创建线程和销毁线程 b) 提高响应速度,任 ...

  8. 本地计算机上的OracleDBConsoleorcl服务启动后停止

    emca -repos dropemca -repos createemca -config dbcontrol db 这三步你都运行成功了也没有报错?最后没有提示你dbcontrol已经启动了么?, ...

  9. canvas语法糖

  10. crunch字典生成

    密码破解基本有三种方法:第一种是人工猜解(垃圾桶工程和被动信息收集): 第二种是基于字典暴力破解(主流) 在kali里,是默认自带了字典的,分别放在下面三个文件中:/usr/share/wordlis ...