-
Notifications
You must be signed in to change notification settings - Fork 3
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
「14」JavaScript 函数式编程(三) #15
Comments
朋友,看你文章挺有意思的,不过就是感觉更新比较慢,能多更新一些有趣的内容吗?? |
最近在搞 tua-storage 和 tua-api...,之后写写安利文 |
你好,你有一个地方输出的结果写错了 在Boolean也满足半群的那个 输出结果应该是false |
为什么我看你的函数式编程有一股离散数学的味道 |
感谢校对,已修正 |
亲爱的作者,幺半群那里的Boolean的单位元是否写反了? |
下面的max 与 min 函数是否也出错了 |
没写反
简单来说也就是单位元和半群中的数进行运算,结果还是那个数。(下面的运算中第一个数是单位元)
|
离散没学好.... 单位元的本质来说就是通过运算并不会改变它运算元素 是我的问题!!!😅
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 = e ○ a
按照这个定义是不是单位元:对于半群 <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 应用
4.10.Traversable
1.问题引入
然而我们想得到的是一个包含数组的
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
的同构关系参考资料
Monad, Monoid, Applicative, Functor ??
以上 to be continued...
The text was updated successfully, but these errors were encountered: