边城

JavaScript中的函数式编程 | Lifelong Learning

边城 · 2017-01-25翻译 · 820阅读 原文链接

介绍

最近我阅读了这一有趣的文章 弗里斯比教授最好的函数式编程教程,我想在这篇文章中说说我的理解。

f(x)

命令式编程中,你会向计算机发送一系列的任务让它执行完成。在执行的过程中,状态有可能发生变化。而在 完全的函数式编程 中你不会告诉计算机他要做什么,而是告诉它 什么。

> // 命令式
> 
> var original = [1,2,3,4,5]
> var squared = []
> 
> for(var i = 0; i < original.length; i++) {  
>   var squaredNumber = original[i] * original[i]  
>   squared.push(squaredNumber) 
> } 
> 
> console.log(squared) //=> [1, 4, 9, 16, 25]
>
> // 函数式
> 
> var original = [1,2,3,4,5]
> 
> var squared = original.map(n => n * n);
> 
> console.log(squared) //=> [1, 4, 9, 16, 25]
>

函数属于第一类

在 JavaScript 中,函数是“第一类”,意思是他们和其它类型一样,比如 Numnber、String 等。

最好是纯函数

纯函数是这样一种函数,只要给它相同的输入,它就会返回相同的输出, 而且不会有任何可观察到的副作用。

副作用包含但不限于如下这些:

改变文件系统 对数据库进行插入/更新/删除记录的操作 调用 HTTP 突变 向屏幕打印/记录日志 获取用户输入 查询 DOM 访问系统状态

8年级的数学

来自 mathisfun.com:

函数是值和值之间的特定关系:它对每组输入值都返回精确而唯一的输出值。

定义一个计算“平方”的函数

> function square(x) {
>   return x * x;
> }
>

如果像上面说的那样,函数只是值与值之间的特定关系,我们可以从下面的表格看到这个关系:

screen-shot-2017-01-10-at-10-54-08-am

如果输入决定输出,就不需要实现细节。纯函数 数学函数,也是函数式编程的全部内容。

纯函数是:

  • 可缓存的

  • 自解释的

  • 可测试的

  • 合理的(引用透明:如果一段代码可以用其计算出来的值代替而不会影响程序的行为,那它就是引用透明的。)

  • 不能并行执行

柯里化函数

柯里化函数的概念是,你可以用比某个函数期望的参数更少的参数来调用它。它会返回一个新函数用于接收余下的参数。

比如,一个柯里化的相加函数会是这样:

> function add = a => b => {
>     return a+b;
> }
> 
> add(1)(2) // 结果: 3
>

前面提到一个函数是值与值之间的特定关系:它对每组输入值都返回精确而唯一的输出值。我们可以使用柯里化函数和少量参数产生一个有用的新函数,记住这样一个概念:尽管有多个参数,数学函数定义会让每个参数映射到准确的一个输出。这就是为什么Haskell官方的每一个函数都只有一个参数的原因。所有接受多个参数的函数都可以变成柯里化函数

组合 – 圣杯Holy Grail

组合函数可以这样定义:

> var compose = function(f, g) {
>   return function(x) {
>     return f(g(x));
>   };
> };
>

fg 都是函数,x 从他们中间通过。

FP 中最重要的构建块是函数。组合允许我们合并这些小的构建块来构建更大的程序。如果你在寻找函数定义,compose 函数有两个函数作为参数,fg,它创建了一个 新的函数,这个新函数将 x 传递给 g 进行调用,然后再将 g 的结果传递给 f,最终返回 f 的结果。

函数组合是一种结合

> // 结合律
> 
> var associative = 
> compose(f, compose(g, h)) == compose(compose(f, g), h);
> 
> // true
>

这就是说我们不需要考虑如何组织 compose 调用,反正调用结果都是一样的。这一特性让我们可以写复杂的组合(像在 lodashramda 等库中实现的那样)。

最好的类比思考是强大的UNIX管道。UNIX有两个重要的思想:

  1. 写只做一件事并把这件事做好的程序。

  2. 写能在一起工作的程序。

例如:

find - 遍历文件树 cat - 连接和打印文件 grep - 通过模板匹配搜索文件 xargs - 构造参数列表并调用工具

上面提到的每一个程序都把一件事做得很好。而且把它们合并起来可以得到一个强大的程序。

find . -name "fp.txt" | xargs cat | grep "FP"

从当前目录查找一个叫“fp.txt”的文件,通过 xargs 将结果传递给 cat,再将得到的内容传递给 grep 用于搜索“FP”

在上面的FP程序中,我们写了只做一件事并把事情做好的的纯函数,然后把这些函数组合起来使他们能在一起工作。

Hindley-Milner

在函数式编程的世界,我们要不了多久就会发现自己深陷类型签名的泥潭。类型是种元语言元素,它使不同背景的人能一起在简洁而高效的沟通。多数情况下,他们使用一个叫“Hindley-Milner”的类型系统来写程序。

> 以下参数的类型签名如下:
> 
> :t capitalize
> capitalize :: String -> String
> 
> :t strLength
> strLength :: String -> Number
> 
> :t head
> head :: [a] -> a
> 
> :t reverse
> reverse :: [a] -> [a]
> 
> :t sort
> sort :: Ord a => [a] -> [a] :t foldl
> foldl :: Foldable t => (b -> a -> b) -> b -> t a -> b
>

一旦引入一个类型变量,就会出现一种称为参数化的神奇属性。这个属性说明函数会以统一的方式作用于所有类型,这种方式可以使函数大大缩减其多态类型

容器/盒子/计算上下文

我们已经看到如何将函数应用到一个值。

> function addOne(x) {
>   return x + 1;
> }
> 
> var r = addOne(10) // r = 11
>

我们扩展一下这个想法,假如上下文中可以是任何值。现在我们可以认为这是一个容器盒子计算上下文

你可以像下面的定义那样创建这个类型:

> var Container = function(x) {
>   this.__value = x;
> }
> 
> Container.of = function(x) { return new Container(x); };
>

我们产生一个名为 Maybe 的简单类型,它有 2 个不同的状态:一个状态包含某个值,另一个是 Nothing。在 Haskell 中,像下面定义这个类型

> data Maybe a = Nothing | Just a
>

Scala 中这种类型称为 Option,它可能是两种状态:SomeNone

JavaScript 中,像下面的示例那们,我们不过定义了 Maybe 类,封装了两种类型。在实践中,我们应该反映 Haskell 或 Scala 或其它语言对每种状态有不同的构建器。

> var Maybe = function(x) {
>   this.__value = x;
> };
> 
> Maybe.of = function(x) {
>   return new Maybe(x);
> };
> 
> Maybe.prototype.isNothing = function() {
>   return (this.__value === null || this.__value === undefined);
> };
> 
> Maybe.of("JS rocks!!"); // 包含一个值的状态
> Maybe.of(null); // 包含 null 状态
>

Functor

Functor 的定义如下:

> map :: Functor f => (a -> b) -> f a -> f b
>

从签名中,我们可以清楚地看到,Functor 定义一个映射方法,其实现必须提供一个 map 函数的实现。从签名中我们还能看到映射方法让我们对包含在计算上下文中的值进行数据传输map 函数另一个非常重要的属性是保留结构

Functor 实现了 map 并遵循一些规律

Maybe 是个 Functor,它提供对 map 的实现。

> Maybe.prototype.map = function(f) {
>   return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
> };
> 
> Maybe.of('Mayakumar').map(match(/a/ig));
> // => Maybe['a', 'a', 'a'] 
>

Functor 定律

> // 标识
> map(id) === id;
> 
> // 组合
> compose(map(f), map(g)) === map(compose(f, g));
>

下图展示了在 Functormap 方法中发生的事情:

functor

Monad

指向性的 Factory 工厂

of 方法用于放置一个值,它在一个被称为指向的非常重要接口的最小上下文中调用。

一个 指向的 Functor 是有 of 方法的 Functor

> Maybe.of(100).map(add(1));
> // Maybe(101)
>

如果 map 函数拥有 a => M[B] 这样的签名,我们会得到嵌入容器的类型。如果我们必须获得值,就得进行多次映射,至于多少次得从调用者的角度看。

我们有时候需要移除额外的容器/盒子/计算上下文,M[M[a]M[a]

> var mmo = Maybe.of(Maybe.of('value'));
> // Maybe(Maybe('value'))
> 
> mmo.join();
> // Maybe('value')
>
> Monads 是可以扁平化的有指向的 Functor
>

字义了 join 方法的 Functor,如果有 of 方法,并服从一些规则,它就是 Monad。定义 join 并不困难,看看在 Maybe 中定义:

> Maybe.prototype.join = function() {
>   return this.isNothing() ? Maybe.of(null) : this.__value;
> }
>

我们可以在 map 之后立即调用 join,这种做法抽象为“chian”。

> //  chain :: Monad m => (a -> m b) -> m a -> m b
> var chain = curry(function(f, m){
>   return m.map(f).join(); // or compose(join, map(f))(m)
> });
>

对于 Maybe:

> Maybe.prototype.chain = function(f) {
>   return this.isNothing() ? Maybe.of(null) : this.map(f).join();
> }
>

chain 可以嵌套,我们可以以纯函数的方式发生 序列变量赋值

For eg:

> var result = Maybe.of(3).chain(a => {
>    return Maybe.of(30).chain(b => {
>       return Maybe.of(300).chain(c => {
>         return Maybe.of(3000).map(d => a + b + c + d);
>       });
>    })
> });
> 
> // => 3333
>

Monad 定律

> // 结合
> compose(join, map(join)) == compose(join, join);
> 
> // 所有 (M a) 一致
> compose(join, of) === compose(join, map(of)) === id var mcompose = function(f, g) {
>   return compose(chain(f), g);
> };
> 
> // 左向一致
> mcompose(M, f) == f;
> 
> // 右向一致
> mcompose(f, M) == f;
> 
> // 结合
> mcompose(mcompose(f, g), h) === mcompose(f, mcompose(g, h));
>

下图展示了 Monad 的 chain 方法是如何运作的:

> function even(x) {
>    if(x % 2 === 0) {
>       return Maybe.of(x/2);
>     } else {
>       return Maybe.of(null);
>     }
> }
>

monad

Applicatives

Applicatives 提供 Functor 间相互应用的能力。下面的示例可以帮助理解:

> var add = a => b => {
>  return a + b;
> }
> 
> add(Maybe.of(2), Maybe.of(3));
> // Not possible
> 
> var maybe_of_add_2 = map(add, Maybe.of(2));
> // Maybe(add(2))
> 
>

我们自己的 Maybe 拥有一些应用于内部的函数。更特殊的情况下, 匀拥有Maybe(add(2)) ,并希望应用它的 add(2)Maybe(3)3 上来完成调用。换句话说,我们想在某个 Functor 上应用其它 Functor。我们可以通过使用 chianamp 函数来实现:

> Maybe.of(2).chain(function(two) {
>  return Maybe.of(3).map(add(two));
> });
>

这里的问题在于,我们被困在 Monad 的序列中了。我们有两个独立的值,并且我们认为没必要仅仅为满足 Monad 的顺序要求就延迟创建 Maybe(3)因此 Applicatives 提供了这样一种功能,我们需要在计算上下文内部对值使用函数,但这些值是独立而无序的。这有一个不错的应用场景:Applicatives 用于并行独立的异步计算,然后将所有结果包含在一个 Functor 中。

> Maybe.prototype.ap = function(other_container) {
>  return this.isNothing() ? this : other_container.map(this.__value);
> }
> 
> Maybe.of(add).ap(Maybe.of(2)).ap(Maybe.of(3));
> // Maybe 5
>
> _Applicative Functor_ 是拥有 ap 方法的指向性 Functor。
>

Applicative 定律

> // 一致性
> A.of(id).ap(v) == v // 形态相同
> A.of(f).ap(A.of(x)) == A.of(f(x)) // 交换律
> v.ap(A.of(x)) == A.of(function(f) { return f(x) }).ap(v) // 组合
> A.of(compose).ap(u).ap(v).ap(w) == u.ap(v.ap(w));
>

Applicatives 的一个比较好的用例是具有多个 Functor 参数的情况。在 Functor 的世界,它们给我们将函数作为参数的能力。虽然可以用 Monad 这样做,但我们不依赖特定功能的情况下宁愿用 Applicative Functor。

下图展示了 Applicativeap 方法:

applicatives

map/of/chain/ap

我们已经解释了 Functor、Monad、Applicative 的概念。这很多结构服从/满足 Functor、Monad 或 Applicative 的性质。从上面的例子中我们可以看到 Maybe 是 Functor、Monad 和 Applicative。可以作为 Functor、Monad、Applicative 的结构包括:

> List
> Maybe
> IO
> Task (https://github.com/folktale/data.task)
> 等
>

如果某个类型是 Monad,它一定是 AppliacativeFunctor。如果某个类型是 Applicative,它一定是 Functor

概括

弗里斯比教授最好的函数式编程教程 是我最近读到的最好的文章。作者在其中神奇地展示了函数式编程的强大和深奥,也解释了它如何应用于 JavaScript 语言。

参考

  1. https://www.gitbook.com/book/drboolean/mostly-adequate-guide/details

  2. http://learnyouahaskell.com/

  3. http://adit.io/posts/2013-04-17-functors,_applicatives,_and_monads_in_pictures.html (The diagrammatic representation for Functor, Monad, Applicatives are inspired from this post).

分享这篇文章:

相关文章