Open BuptStEve opened 6 years ago
朋友,看你文章挺有意思的,不过就是感觉更新比较慢,能多更新一些有趣的内容吗??
朋友,看你文章挺有意思的,不过就是感觉更新比较慢,能多更新一些有趣的内容吗??
最近在搞 tua-storage 和 tua-api...,之后写写安利文 文章这东西有输入才能有输出嘛~
你好,你有一个地方输出的结果写错了 在Boolean也满足半群的那个 输出结果应该是false
为什么我看你的函数式编程有一股离散数学的味道
你好,你有一个地方输出的结果写错了 在Boolean也满足半群的那个 输出结果应该是false
感谢校对,已修正
亲爱的作者,幺半群那里的Boolean的单位元是否写反了?
下面的max 与 min 函数是否也出错了
亲爱的作者,幺半群那里的Boolean的单位元是否写反了? 下面的max 与 min 函数是否也出错了
没写反
单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
简单来说也就是单位元和半群中的数进行运算,结果还是那个数。(下面的运算中第一个数是单位元)
true && true = true
, true && false = false
false || true = true
, false || false = false
Math.min(Infinity, 100) = 100
Math.max(-Infinity, 100) = 100
离散没学好.... 单位元的本质来说就是通过运算并不会改变它运算元素 是我的问题!!!😅
StEve Young notifications@github.com 于2019年3月3日周日 上午11:17写道:
亲爱的作者,幺半群那里的Boolean的单位元是否写反了? 下面的max 与 min 函数是否也出错了
没写反
单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
简单来说也就是单位元和半群中的数进行运算,结果还是那个数。(下面的运算中第一个数是单位元)
- 对于 <Boolean, &&> 来说单位元就是 true, true && true = true, true && false = false
- 对于 <Boolean, ||> 来说单位元就是 false, false || true = true, false || false = false
- 对于 <Number, Min> 来说单位元就是 Infinity, Math.min(Infinity, 100) = 100
- 对于 <Number, Max> 来说单位元就是 -Infinity, Math.max(-Infinity, 100) = 100
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/BuptStEve/blog/issues/15#issuecomment-468983518, or mute the thread https://github.com/notifications/unsubscribe-auth/Aon5gvxhEvYjzR8Xzp54eetdoIBINlh_ks5vSz6sgaJpZM4V4GKa .
单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
离散没学好.... 单位元的本质来说就是通过运算并不会改变它运算元素 是我的问题!!!😅 StEve Young notifications@github.com 于2019年3月3日周日 上午11:17写道: … 亲爱的作者,幺半群那里的Boolean的单位元是否写反了? 下面的max 与 min 函数是否也出错了 没写反 单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a 简单来说也就是单位元和半群中的数进行运算,结果还是那个数。(下面的运算中第一个数是单位元) - 对于 <Boolean, &&> 来说单位元就是 true, true && true = true, true && false = false - 对于 <Boolean, ||> 来说单位元就是 false, false || true = true, false || false = false - 对于 <Number, Min> 来说单位元就是 Infinity, Math.min(Infinity, 100) = 100 - 对于 <Number, Max> 来说单位元就是 -Infinity, Math.max(-Infinity, 100) = 100 — You are receiving this because you commented. Reply to this email directly, view it on GitHub <#15 (comment)>, or mute the thread https://github.com/notifications/unsubscribe-auth/Aon5gvxhEvYjzR8Xzp54eetdoIBINlh_ks5vSz6sgaJpZM4V4GKa .
按照这个定义是不是单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = a?
单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a
离散没学好.... 单位元的本质来说就是通过运算并不会改变它运算元素 是我的问题!!!😅 StEve Young notifications@github.com 于2019年3月3日周日 上午11:17写道: … 亲爱的作者,幺半群那里的Boolean的单位元是否写反了? 下面的max 与 min 函数是否也出错了 没写反 单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = e ○ a 简单来说也就是单位元和半群中的数进行运算,结果还是那个数。(下面的运算中第一个数是单位元) - 对于 <Boolean, &&> 来说单位元就是 true, true && true = true, true && false = false - 对于 <Boolean, ||> 来说单位元就是 false, false || true = true, false || false = false - 对于 <Number, Min> 来说单位元就是 Infinity, Math.min(Infinity, 100) = 100 - 对于 <Number, Max> 来说单位元就是 -Infinity, Math.max(-Infinity, 100) = 100 — You are receiving this because you commented. Reply to this email directly, view it on GitHub <#15 (comment)>, or mute the thread https://github.com/notifications/unsubscribe-auth/Aon5gvxhEvYjzR8Xzp54eetdoIBINlh_ks5vSz6sgaJpZM4V4GKa .
按照这个定义是不是单位元:对于半群 <S, ○>,存在 e ∈ S,使得任意 a ∈ S 有 a ○ e = a?
对的,https://zh.wikipedia.org/zh-hans/%E5%96%AE%E4%BD%8D%E5%85%83
slide 地址
四、Talk is cheap!Show me the ... MONEY!
4.1.容器(Box)
假设有个函数,可以接收一个来自用户输入的数字字符串。我们需要对其预处理一下,去除多余空格,将其转换为数字并加一,最后返回该值对应的字母。代码大概长这样...
因缺思厅,这代码嵌套的也太紧凑了,看多了“老阔疼”,赶紧重构一把...
很显然,经过之前内容的熏(xi)陶(nao),一眼就可以看出这个修订版代码很不 Pointfree...
为了这些只用一次的中间变量还要去想或者去查翻译,也是容易“老阔疼”,再改再改~
这次借助数组的 map 方法,我们将必须的4个步骤拆分成了4个小函数。
这样一来再也不用去想中间变量的名称到底叫什么,而且每一步做的事情十分的清晰,一眼就可以看出这段代码在干嘛。
我们将原本的字符串变量 str 放在数组中变成了 [str],这里就像放在一个容器里一样。
不过在这里我们可以更进一步,让我们来创建一个新的类型 Box。我们将同样定义 map 方法,让其实现同样的功能。
此外创建一个容器,除了像函数一样直接传递参数以外,还可以使用静态方法
of
。其实这个
Box
就是一个函子(functor),因为它实现了map
函数。当然你也可以叫它Mappable
或者其他名称。不过为了保持与范畴学定义的名称一致,我们就站在巨人的肩膀上不要再发明新名词啦~(后面小节的各种奇怪名词也是来源于数学名词)。
那么这些特定的规则具体是什么咧?
1. 规则一:
这其实就是函数组合...
2. 规则二:
4.2.Either / Maybe
假设现在有个需求:获取对应颜色的十六进制的 RGB 值,并返回去掉
#
后的大写值。以上代码在输入已有颜色的
key
值时运行良好,不过一旦传入其他颜色就会报错。咋办咧?暂且不提条件判断和各种奇技淫巧的错误处理。咱们来先看看函数式的解决方案~
函数式将错误处理抽象成一个
Either
容器,而这个容器由两个子容器Right
和Left
组成。可以看出
Right
和Left
相似于Box
:fold
函数,这里需要传两个回调函数,左边的给Left
使用,右边的给Right
使用。Left
的map
函数忽略了传入的函数(因为出错了嘛,当然不能继续执行啦)。现在让我们回到之前的问题来~
从以上代码不知道各位读者老爷们有没有看出使用
Either
的好处,那就是可以放心地对于这种类型的数据进行任何操作,而不是在每个函数里面小心翼翼地进行参数检查。4.3.
Chain
/FlatMap
/bind
/>>=
假设现在有个 json 文件里面保存了端口,我们要读取这个文件获取端口,要是出错了返回默认值 3000。
so easy~,下面让我们来用 Either 来重构下看看效果。
以上代码有个
bug
,当json
文件写的有问题时,在JSON.parse
时会出错,所以这步也要用tryCatch
包起来。但是,问题来了...
返回值这时候可能是
Right(Right(''))
或者Right(Left(e))
(想想为什么不是Left(Right(''))
或者Left(Left(e)))
。也就是说我们现在得到的是两层容器,就像俄罗斯套娃一样...
要取出容器中的容器中的值,我们就需要
fold
两次...!(若是再多几层...)因缺思厅,所以聪明机智的函数式又想出一个新方法 chain~,其实很简单,就是我知道这里要返回容器了,那就不要再用容器包了呗。
其实这里的
Left
和Right
就是单子(Monad),因为它实现了chain
函数。在继续介绍这些特定规则前,我们先定义一个
join
函数:这条规则说明了
map
可被chain
和of
所定义。也就是说
Monad
一定是Functor
Monad
十分强大,之后我们将利用它处理各种副作用。但别对其感到困惑,chain
的主要作用不过将两种不同的类型连接(join
)在一起罢了。4.4.半群(Semigroup)
举例来说,JavaScript 中有 concat 方法的对象都是半群。
虽然理论上对于
<Number, +>
来说它符合半群的定义:但是数字并没有 concat 方法
没事儿,让我们来实现这个由
<Number, +>
组成的半群 Sum。除此之外,
<Boolean, &&>
也满足半群的定义~最后,让我们对于字符串创建一个新的半群 First,顾名思义,它会忽略除了第一个参数以外的内容。
这个问题留给下个小节。在此先说下这玩意儿有啥用。
假设有两个数据,需要将其合并,那么利用半群,我们可以对 name 应用 First,对于 isPaid 应用 All,对于 points 应用 Sum,最后的 friends 已经是半群了...
4.5.幺半群(Monoid)
半群我们都懂,不过啥是单位元?
举例来说,对于数字加法这个半群来说,0就是它的单位元,所以
<Number, +, 0>
就构成一个幺半群。同理:<Number, *>
来说单位元就是 1<Boolean, &&>
来说单位元就是 true<Boolean, ||>
来说单位元就是 false<Number, Min>
来说单位元就是 Infinity<Number, Max>
来说单位元就是 -Infinity那么
<String, First>
是幺半群么?显然我们并不能找到这样一个单位元 e 满足
First(e).concat(First('steve')) === First('steve').concat(First(e))
这就是上一节留的小悬念,为何会感觉 First 与 Sum 和 All 不太一样的原因。
其实看到幺半群的第一反应应该是默认值或初始值,例如 reduce 函数的第二个参数就是传入一个初始值或者说是默认值。
从以上代码可以看出幺半群比半群要安全得多,
4.6.foldMap
1.套路
在上一节中幺半群的使用代码中,如果传入的都是幺半群实例而不是原始类型的话,你会发现其实都是一个套路...
所以对于思维高度抽象的函数式来说,这样的代码肯定是需要继续重构精简的~
2.List、Map
在讲解如何重构之前,先介绍两个炒鸡常用的不可变数据结构:
List
、Map
。顾名思义,正好对应原生的
Array
和Object
。3.利用 List、Map 重构
因为
immutable
库中的List
和Map
并没有empty
属性和fold
方法,所以我们首先扩展 List 和 Map~这样一来上一节的代码就可以精简成这样:
4.利用 foldMap 重构
注意到
map
和fold
这两步操作,从逻辑上来说是一个操作,所以我们可以新增foldMap
方法来结合两者。所以最终版长这样:
4.7.LazyBox
下面我们要来实现一个新容器
LazyBox
。顾名思义,这个容器很懒...
虽然你可以不停地用
map
给它分配任务,但是只要你不调用fold
方法催它执行(就像deadline
一样),它就死活不执行...4.8.Task
1.基本介绍
有了上一节中
LazyBox
的基础之后,接下来我们来创建一个新的类型Task。
首先
Task
的构造函数可以接收一个函数以便延迟计算,当然也可以用of
方法来创建实例,很自然的也有map
、chain
、concat
、empty
等方法。与众不同的是它有个
fork
方法(类似于LazyBox
中的fold
方法,在fork
执行前其他函数并不会执行),以及一个rejected
方法,类似于Left
,忽略后续的操作。2.使用示例
接下来让我们做一个发射飞弹的程序~
3.原理意义
上面的代码乍一看好像没啥用,只不过是把待执行的代码用函数包起来了嘛,这还能吹上天?
还记得前面章节说到的副作用么?虽然说使用纯函数是没有副作用的,但是日常项目中有各种必须处理的副作用。
所以我们将有副作用的代码给包起来之后,这些新函数就都变成了纯函数,这样我们的整个应用的代码都是纯的~,并且在代码真正执行前(
fork
前)还可以不断地compose
别的函数,为我们的应用不断添加各种功能,这样一来整个应用的代码流程都会十分的简洁漂亮。4.异步嵌套示例
以下代码做了 3 件事:
让我们用 Task 来改写一下~
代码一目了然,按照线性的先后顺序完成了任务,并且在其中还可以随意地插入或修改需求~
4.9.Applicative Functor
1.问题引入
Applicative Functor
提供了让不同的函子(functor)互相应用的能力。先来看个简单例子:
现在我们有了一个容器,它的内部值为局部调用(partially applied)后的函数。接着我们想让它应用到
Box(3)
上,最后得到Box(5)
的预期结果。说到从容器中取值,那肯定第一个想到
chain
方法,让我们来试一下:成功实现~,BUT,这种实现方法有个问题,那就是单子(Monad)的执行顺序问题。
我们这样实现的话,就必须等
Box(2)
执行完毕后,才能对Box(3)
进行求值。假如这是两个异步任务,那么完全无法并行执行。2.基本介绍
下面介绍下主角:
ap
~:运算规则
3.Lift 家族
由于日常编写代码的时候直接用 ap 的话模板代码太多,所以一般通过使用 Lift 家族系列函数来简化。
4.Lift 应用
const getScreenSize = screen => head => foot => screen - (head.height + foot.height)
liftA2(getScreenSize(800))($('header'))($('footer')) // Right(780)
const reportHeader = (p1, p2) =>
Report: ${p1.title} compared to ${p2.title}
Task.of(p1 => p2 => reportHeader(p1, p2)) .ap(Db.find(20)) .ap(Db.find(8)) .fork(console.error, console.log) // Report: 20 compared to 8
liftA2 (p1 => p2 => reportHeader(p1, p2)) (Db.find(20)) (Db.find(8)) .fork(console.error, console.log) // Report: 20 compared to 8
然而我们想得到的是一个包含数组的
Task([file1, file2])
,这样就可以调用它的fork
方法,查看执行结果。为了解决这个问题,函数式编程一般用一个叫做
traverse
的方法来实现。traverse
方法第一个参数是创建函子的函数,第二个参数是要应用在函子上的函数。2.实现
其实以上代码有
bug
...,因为数组 Array 是没有traverse
方法的。没事儿,让我们来实现一下~不急,首先看代码主体是一个
reduce
,这个很熟了,就是从左到右遍历元素,其中的第二个参数传递的就是幺半群(monoid)的单位元(empty)。再看第一个参数,主要就是通过
applicative functor
调用ap
方法,再将其执行结果使用concat
方法合并到数组中。所以最后返回的就是
Task([foo, bar])
,因此我们可以调用fork
方法执行它。4.11.自然变换(Natural Transformations)
1.基本概念
自然变换就是一个函数,接受一个函子(functor),返回另一个函子。看看代码熟悉下~
这个
boxToEither
函数就是一个自然变换(nt),它将函子Box
转换成了另一个函子Either
。答案是不行!
因为自然变换不仅是将一个函子转换成另一个函子,它还满足以下规则:
举例来说就是:
即先对函子
a
做改变再将其转换为函子b
,是等价于先将函子a
转换为函子b
再做改变。显然,
Left
并不满足这个规则。所以任何满足这个规则的函数都是自然变换。2.应用场景
1.例1:得到一个数组小于等于 100 的最后一个数的两倍的值
根据自然变换,它显然和
first(getLargeNums(arr)).map(double)
是等价的。但是后者显然性能好得多。再来看一个更复杂一点儿的例子:
2.例2:找到 id 为 3 的用户的最好的朋友的 id
这是什么鬼???
肯定不能这么干...
第二版的问题是多余的嵌套代码。
第三版的问题是多余的重复逻辑。
4.12.同构(Isomorphism)
简单来说就是两种不同类型的对象经过变形,保持结构并且不丢失数据。
具体怎么做到的呢?
其实同构就是一对儿函数:
to
和from
,遵守以下规则:这其实说明了这两个类型都能够无损地保存同样的信息。
1. 例如
String
和[Char]
就是同构的。这能有啥用呢?
2. 再来看看最多有一个参数的数组
[a]
和Either
的同构关系参考资料
以上 to be continued...