前言

JavaScript是一门多范式语言,即可使用OOP(面向对象),也可以使用FP(函数式),由于笔者最近在学习React相关的技术栈,想进一步深入了解其思想,所以学习了一些FP相关的知识点,本文纯属个人的读书笔记,如果有错误,望轻喷且提点。

什么是函数式编程

函数式编程(英语:functional programming)或称函数程序设计、泛函编程,是一种编程范式,它将计算机运算视为函数运算,并且避免使用程序状态以及易变对象。即对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。

为什么Js支持FP

Js支持FP的一个重要原因在于,在JS中,函数是一等公民。即你可以像对其他数据类型一样对其进行操作,把他们存在数组里,当作参数传递,赋值给变量...等等。如下:

  1. const func = () => {}
  2. // 存储
  3. const a = [func]
  4. // 参数 返回值
  5. const x = (func) => {
  6. ......
  7. ......
  8. return func
  9. }
  10. x(func)

这个特性在编写语言程序时带来了极大的便利,下面的知识及例子都建立在此基础上。

纯函数

概念

纯函数是这样一种函数,即相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用。

副作用包括但不限于:

  • 打印/log
  • 发送一个http请求
  • 可变数据
  • DOM查询

    简单一句话, 即只要是与函数外部环境发生交互的都是副作用。

像Js中, slice就是纯函数, 而splice则不是

  1. var xs = [1,2,3,4,5];
  2. // 纯的
  3. xs.slice(0,3);
  4. //=> [1,2,3]
  5. xs.slice(0,3);
  6. //=> [1,2,3]
  7. xs.slice(0,3);
  8. //=> [1,2,3]
  9. // 不纯的
  10. xs.splice(0,3);
  11. //=> [1,2,3]
  12. xs.splice(0,3);
  13. //=> [4,5]
  14. xs.splice(0,3);
  15. //=> []

例子

在React生态中,使用纯函数的例子很常见,如React Redner函数,Redux的reducer,Redux-saga的声明式effects等等。

React Render

在React中,Render返回了一个JSX表达式,只要输入相同,即可以保证我们拿到同样的输出(最终结果渲染到DOM上),而内部的封装细节我们不需要关心,只要知道它是没有副作用的,这在我们开发过程中带来了极大的便利。当我们的程序出问题时(渲染出来与预期不符合),我们只要关心我们的入参是否有问题即可。

  1. class Component extends React.Component {
  2. render() {
  3. return (
  4. <div />
  5. )
  6. }
  7. }

Redux的reducer

Redux的reducer函数要求我们每一次都要返回一个新的state, 并且在其中不能有任何副作用,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。这样做可以使得我们很容易的保存了每一次state改变的情况,对于时间旅行这种需求更是天然的亲近。特别是在调试的过程中,我们可以借助插件,任意达到每一个state状态,能够轻松的捕捉到错误是在哪一个节点出现。

  1. function todoApp(state = initialState, action) {
  2. switch (action.type) {
  3. case SET_VISIBILITY_FILTER:
  4. return Object.assign({}, state, {
  5. visibilityFilter: action.filter
  6. })
  7. case ADD_TODO:
  8. return Object.assign({}, state, {
  9. todos: [
  10. ...state.todos,
  11. {
  12. text: action.text,
  13. completed: false
  14. }
  15. ]
  16. })
  17. default:
  18. return state
  19. }
  20. }

Redux-sage的声明式effects

许多时候, 我们会写这样的函数

  1. const sendRequest = () => {
  2. return axions.post(...)
  3. }

这是一个不纯的函数,因为它包含了副作用,发起了http请求,我们可以这样封装一下:

  1. const sendRequestReducer = () => {
  2. return () => {
  3. return axios.post(...)
  4. }
  5. }

ok, 现在是一个纯函数了,正如Redux-saga中的effects一样:

  1. import { call } from 'redux-saga/effects'
  2. function* fetchProducts() {
  3. const products = yield call(Api.fetch, '/products')
  4. // ...
  5. }

实际上call不立即执行异步调用,相反,call 创建了一条描述结果的信息。那么这样做除了增加代码的复杂度,还可以给我们带来什么?参考saga的官方文档就知道了, 答案是测试:

这些 声明式调用(declarative calls) 的优势是,我们可以通过简单地遍历 Generator 并在 yield 后的成功的值上面做一个 deepEqual 测试, 就能测试 Saga 中所有的逻辑。这是一个真正的好处,因为复杂的异步操作都不再是黑盒,你可以详细地测试操作逻辑,不管它有多么复杂。

  1. import { call } from 'redux-saga/effects'
  2. import Api from '...'
  3. const iterator = fetchProducts()
  4. // expects a call instruction
  5. assert.deepEqual(
  6. iterator.next().value,
  7. call(Api.fetch, '/products'),
  8. "fetchProducts should yield an Effect call(Api.fetch, './products')"
  9. )

总结

纯函数有着以下的优点

可缓存性

首先,纯函数总能够根据输入来做缓存。实现缓存的一种典型方式是 memoize 技术:

  1. var memoize = function(f) {
  2. var cache = {};
  3. return function() {
  4. var arg_str = JSON.stringify(arguments);
  5. cache[arg_str] = cache[arg_str] || f.apply(f, arguments);
  6. return cache[arg_str];
  7. };
  8. };
  9. var squareNumber = memoize(function(x){ return x*x; });
  10. squareNumber(4);
  11. //=> 16
  12. squareNumber(4); // 从缓存中读取输入值为 4 的结果
  13. //=> 16
  14. squareNumber(5);
  15. //=> 25
  16. squareNumber(5); // 从缓存中读取输入值为 5 的结果
  17. //=> 25

可移植性

纯函数因为不依赖外部环境,所以非常便于移植,你可以在任何地方使用它而不需要附带着引入其他不需要的属性。

可测试性

如上面提到的Redux reducer和Redux-saga一样, 它对于测试天然亲近。

并行代码

我们可以并行运行任意纯函数。因为纯函数根本不需要访问共享的内存,而且根据其定义,纯函数也不会因副作用而进入竞争态(race condition)。

柯里化

概念

在计算机科学中,柯里化(英语:Currying),又译为卡瑞化或加里化,是把接受多个参数的函数变换成接受一个单一参数(最初函数的第一个参数)的函数,并且返回接受余下的参数而且返回结果的新函数的技术

  1. var add = function(x) {
  2. return function(y) {
  3. return x + y;
  4. };
  5. };
  6. var increment = add(1);
  7. var addTen = add(10);
  8. increment(2);
  9. // 3
  10. addTen(2);
  11. // 12

例子

在Lodash类库中,就有这么一个curry函数来帮助我们处理科里化,关于如何实现一个curry函数,推荐大家参考这篇文章

  1. var abc = function(a, b, c) {
  2. return [a, b, c];
  3. };
  4. var curried = _.curry(abc);
  5. curried(1)(2)(3);
  6. // => [1, 2, 3]
  7. curried(1, 2)(3);
  8. // => [1, 2, 3]
  9. curried(1, 2, 3);
  10. // => [1, 2, 3]
  11. // Curried with placeholders.
  12. curried(1)(_, 3)(2);
  13. // => [1, 2, 3]

偏函数应用

偏函数本身与科里化并不相关, 但在日常的编写程序中,或许我们使用更多的是偏函数,所以在这里简单的介绍一下偏函数

偏函数应用是找一个函数,固定其中的几个参数值,从而得到一个新的函数

有时候,我们会写一个专门发送http请求的函数

  1. const sendRequest = (host, fixPath, path) => {
  2. axios.post(`${host}\${fixPath}\{path}`)
  3. }

但是大多数时候, host和fixPath是固定的, 我们不想每次都写一次host和fixPath,但我们又不能写死,因为我们需要sendRequest这个函数是可以移植的,不受环境的约束,那么我们可以这样

  1. const sendRequestPart = (path) => {
  2. const host = '...'
  3. const fixPath = '...'
  4. return sendRequest(host, fixPath, path)
  5. }

总结

科里化和偏函数的主要用途是在组合中,这一小节主要介绍了他们的使用方法和行为。

组合 compose

组合的功能非常强大, 也是函数式编程的一个核心概念, 所谓的对过程进行封装很大程度上就是依赖于组合。那么什么是组合?

  1. var compose = function(f,g) {
  2. return function(x) {
  3. return f(g(x));
  4. };
  5. };
  6. var toUpperCase = function(x) { return x.toUpperCase(); };
  7. var exclaim = function(x) { return x + '!'; };
  8. var shout = compose(exclaim, toUpperCase);
  9. shout("send in the clowns");
  10. //=> "SEND IN THE CLOWNS!"

上面的compose就是一个最简单的组合函数, 当然组合函数并不限制于传入多少个函数参数,它最后只返回一个函数,我个人更喜欢将它认为像管道一样,将数据经过不同函数的逐渐加工,最后得到我们想要的结果

  1. const testFunc = compose(func1, func2, func3, func4)
  2. testFunc(...args)

在js中, 实现compose函数比较容易

  1. const compose = (...fns) => {
  2. return (...args) => {
  3. let res = args
  4. for (let i = fns.length - 1; i > -1; i--) {
  5. res = fns[i](res)
  6. }
  7. return res
  8. }
  9. }

例子

React官方推崇组合优于继承这个概念,这里选择两个比较典型的例子来看

React中的高阶组件

在React中,有许多使用高阶组件的地方,如React-router的withRouter函数,React-redux的connect函数返回的函数,

  1. // Navbar 和 Comment都是组件
  2. const NavbarWithRouter = withRouter(Navbar);
  3. const ConnectedComment = connect(commentSelector, commentActions)(Comment);

而由于高阶函数的签名是Component => Component。所以我们可以很容易的将他们组合到一起,这也是官方推荐的做法

  1. // 不要这样做……
  2. const EnhancedComponent = withRouter(connect(commentSelector)(WrappedComponent))
  3. // ……你可以使用一个函数组合工具
  4. // compose(f, g, h) 和 (...args) => f(g(h(...args)))是一样的
  5. const enhance = compose(
  6. // 这些都是单独一个参数的高阶组件
  7. withRouter,
  8. connect(commentSelector)
  9. )
  10. const EnhancedComponent = enhance(WrappedComponent)

Redux的compose函数

Redux的compose函数实现要比上面提到的简洁的多

  1. export default function compose(...funcs) {
  2. if (funcs.length === 0) {
  3. return arg => arg
  4. }
  5. if (funcs.length === 1) {
  6. return funcs[0]
  7. }
  8. return funcs.reduce((a, b) => (...args) => a(b(...args)))
  9. }

这个实现咋看之下有点懵逼, 所以可以拆开来看一下

  1. composeFn = compose(fn1, fn2, fn3, fn4)

那么reduce循环运行时, 第一次a就是fn1, b是fn2, 第二次a是(...args) => fn1(fn2(...args)), b是fn3, 第三次运行的时候则是a是(...args) => fn1(fn2(fn3(...args))), b是fn4, 最后返回了fn1(fn2(fn3(fn4(...args))))

pointfree

它的意思是说,函数无须提及将要操作的数据是什么样的。

  1. // 非 pointfree,因为提到了数据:word
  2. var snakeCase = function (word) {
  3. return word.toLowerCase().replace(/\s+/ig, '_');
  4. };
  5. // pointfree
  6. var snakeCase = compose(replace(/\s+/ig, '_'), toLowerCase);

pointfree 模式能够帮助我们减少不必要的命名,让代码保持简洁和通用。对函数式代码来说,pointfree 是非常好的石蕊试验,因为它能告诉我们一个函数是否是接受输入返回输出的小函数。比如,while 循环是不能组合的。不过你也要警惕,pointfree 就像是一把双刃剑,有时候也能混淆视听。并非所有的函数式代码都是 pointfree 的,不过这没关系。可以使用它的时候就使用,不能使用的时候就用普通函数。

总结

有了组合, 配合上面提到的科里化和偏函数应用, 可以将程序拆成一个个小函数然后组合起来, 优点已经很明显的呈现出来,也很直观的表达出了函数式编程的封装过程的核心概念。

范畴学

函数式编程建立在范畴学上,很多时候讨论起来难免有点理论化,所以这里简单的介绍一下范畴。

有着以下这些组件(component)的搜集(collection)就构成了一个范畴:

  • 对象的搜集
  • 态射的搜集
  • 态射的组合
  • identity 这个独特的态射

对象的搜集

对象就是数据类型,例如 String、Boolean、Number 和 Object 等等。通常我们把数据类型视作所有可能的值的一个集合(set)。像 Boolean 就可以看作是 [true, false] 的集合,Number 可以是所有实数的一个集合。把类型当作集合对待是有好处的,因为我们可以利用集合论(set theory)处理类型。

态射的搜集

态射是标准的、普通的纯函数。

态射的组合

即上面提到的compose

identity 这个独特的态射

让我们介绍一个名为 id 的实用函数。这个函数接受随便什么输入然后原封不动地返回它:

  1. var id = function(x){ return x; };

functor

在学习函数式编程的时候,第一次看到functor的时候一脸懵逼, 确实不理解这个东西是什么, 可以做什么,加上一堆术语,头都大了。在理解functor之前,先认识一个东西

概念

容器

容器为函数式编程里普通的变量、对象、函数提供了一层极其强大的外衣,赋予了它们一些很惊艳的特性。

  1. var Container = function(x) {
  2. this.__value = x;
  3. }
  4. Container.of = x => new Container(x);
  5. //试试看
  6. Container.of(1);
  7. //=> Container(1)
  8. Container.of('abcd');
  9. //=> Container('abcd')

Container.of 把东西装进容器里之后,由于这一层外壳的阻挡,普通的函数就对他们不再起作用了,所以我们需要加一个接口来让外部的函数也能作用到容器里面的值(像Array也是一个容器):

  1. Container.prototype.fmap = function(f){
  2. return Container.of(f(this.__value))
  3. }

我们可以这样使用它:

  1. Container.of(3)
  2. .fmap(x => x + 1) //=> Container(4)
  3. .fmap(x => 'Result is ' + x); //=> Container('Result is 4')

我们通过简单的代码就实现了一个链式调用,并且这也是一个functor

Functor(函子)是实现了 fmap 并遵守一些特定规则的容器类型。

这样子看还是有点不好理解, 那么参考下面这句话可能会好一点:

a functor is nothing more than a data structure you can map functions over with the purpose of lifting values from a container, modifying them, and then putting them back into a container. 都是些简单的单词,意会比起本人翻译会更容易理解。

加上一张图:

ok, 现在大概知道functor是一个什么样的东西了。

作用

那么functor有什么作用呢?

链式调用

首先它可以链式调用,正如上面提到的一样。

Immutable

可以看到, 我们每次都是返回了一个新的Container.of, 所以数据是Immutable的, 而Immutable的作用就不在这里赘述了。

将控制权交给Container

将控制权交给Container, 这样他就可以决定何时何地怎么去调用我们传给fmap的function,这个作用非常强大,可以为我们做空值判断、异步处理、惰性求值等一系列麻烦的事。

例子

上面作用的第三点可能直观上有点难以理解, 下面举三个简单的例子

Maybe Container

定义一个Maybe Container来帮我们处理空值的判断

  1. var Maybe = function(x) {
  2. this.__value = x;
  3. }
  4. Maybe.of = function(x) {
  5. return new Maybe(x);
  6. }
  7. Maybe.prototype.fmap = function(f) {
  8. return this.isNothing() ? Maybe.of(null) : Maybe.of(f(this.__value));
  9. }
  10. Maybe.prototype.isNothing = function() {
  11. return (this.__value === null || this.__value === undefined);
  12. }
  13. //试试看
  14. import _ from 'lodash';
  15. var add = _.curry(_.add);
  16. Maybe.of({name: "Stark"})
  17. .fmap(_.prop("age"))
  18. .fmap(add(10));
  19. //=> Maybe(null)
  20. Maybe.of({name: "Stark", age: 21})
  21. .fmap(_.prop("age"))
  22. .fmap(add(10));
  23. //=> Maybe(31)

当然, 这里可以利用上面提到的科里化函数来简化掉一堆fmap的情况

  1. import _ from 'lodash';
  2. var compose = _.flowRight;
  3. var add = _.curry(_.add);
  4. // 创造一个柯里化的 map
  5. var map = _.curry((f, functor) => functor.fmap(f));
  6. var doEverything = map(compose(add(10), _.property("age")));
  7. var functor = Maybe.of({name: "Stark", age: 21});
  8. doEverything(functor);
  9. //=> Maybe(31)

Task Container

我们可以编写一个Task Container来帮我们处理异步的情况

  1. var fs = require('fs');
  2. // readFile :: String -> Task(Error, JSON)
  3. var readFile = function(filename) {
  4. return new Task(function(reject, result) {
  5. fs.readFile(filename, 'utf-8', function(err, data) {
  6. err ? reject(err) : result(data);
  7. });
  8. });
  9. };
  10. readFile("metamorphosis").fmap(split('\n')).fmap(head);

例子中的 reject 和 result 函数分别是失败和成功的回调。正如你看到的,我们只是简单地调用 Task 的 map 函数,就能操作将来的值,好像这个值就在那儿似的。(这看起来有点像Promise)

Io Container

我们可以利用Io Container来做惰性求值

  1. import _ from 'lodash';
  2. var compose = _.flowRight;
  3. var IO = function(f) {
  4. this.__value = f;
  5. }
  6. IO.of = x => new IO(_ => x);
  7. IO.prototype.map = function(f) {
  8. return new IO(compose(f, this.__value))
  9. };
  10. var io_document = new IO(_ => window.document);
  11. io_document.map(function(doc){ return doc.title });
  12. //=> IO(document.title)

注意我们这里虽然感觉上返回了一个实际的值 IO(document.title),但事实上只是一个对象:{ __value: [Function] },它并没有执行,而是简单地把我们想要的操作存了起来,只有当我们在真的需要这个值得时候,IO 才会真的开始求值,

functor 范畴

functor 的概念来自于范畴学,并满足一些定律。 即functor 接受一个范畴的对象和态射(morphism),然后把它们映射(map)到另一个范畴里去

Js中的functor

Js中也有一些实现了functor, 如map、filter

  1. map :: (A -> B) -> Array(A) -> Array(B)
  2. filter :: (A -> Boolean) -> Array(A) -> Array(A)

Monad

普通functor的问题

我们来写一个函数 cat,这个函数的作用和 Linux 命令行下的 cat 一样,读取一个文件,然后打出这个文件的内容

  1. import fs from 'fs';
  2. import _ from 'lodash';
  3. var map = _.curry((f, x) => x.map(f));
  4. var compose = _.flowRight;
  5. var readFile = function(filename) {
  6. return new IO(_ => fs.readFileSync(filename, 'utf-8'));
  7. };
  8. var print = function(x) {
  9. return new IO(_ => {
  10. console.log(x);
  11. return x;
  12. });
  13. }
  14. var cat = compose(map(print), readFile);
  15. cat("file")
  16. //=> IO(IO("file的内容"))

ok, 我们最后得到的是两层嵌套的IO, 要获取其中的值

  1. cat("file").__value().__value()

问题很明显的出来了, 我们需要连续调用两次_value才能获取, 那么假如我们嵌套了更多呢, 难道每次都要调用一大堆__value吗, 那当然是不可能的。

概念

我们可以使用一个join函数, 来将Container里面的东西拿出来, 像这样

  1. var join = x => x.join();
  2. IO.prototype.join = function() {
  3. return this.__value ? IO.of(null) : this.__value();
  4. }
  5. // 试试看
  6. var foo = IO.of(IO.of('123'));
  7. foo.join();

似乎这样也有点麻烦, 每次都要使用一个join来剖析

  1. var doSomething = compose(join, map(f), join, map(g), join, map(h));

我们可以使用一个chain函数, 来帮助我们做这些事

  1. var chain = _.curry((f, functor) => functor.chain(f));
  2. IO.prototype.chain = function(f) {
  3. return this.map(f).join();
  4. }
  5. // 现在可以这样调用了
  6. var doSomething = compose(chain(f), chain(g), chain(h));
  7. // 当然,也可以这样
  8. someMonad.chain(f).chain(g).chain(h)
  9. // 写成这样是不是很熟悉呢?
  10. readFile('file')
  11. .chain(x => new IO(_ => {
  12. console.log(x);
  13. return x;
  14. }))
  15. .chain(x => new IO(_ => {
  16. // 对x做一些事情,然后返回
  17. }))

ok, 事实上这就是一个Monad, 而且你也会很熟悉, 这就像一个Promise的then, 那么什么是Monad呢?

Monad有一个bind方法, 就是上面讲到的chain(同一个东西不同叫法),

  1. function bind<T, U>(instance: M<T>, transform: (value: T) => M<U>): M<U> {
  2. // ...
  3. }

其实,Monad 的作用跟 Functor 类似,也是应用一个函数到一个上下文中的值。不同之处在于,Functor 应用的是一个接收一个普通值并且返回一个普通值的函数,而 Monad 应用的是一个接收一个普通值但是返回一个在上下文中的值的函数。上下文即一个Container。

Promise是Monad

需要被认为是Monad需要具备以下三个条件

  • 拥有容器, 即Maybe、IO之类。
  • 一个可以将普通类型转换为具有上下文的值的函数, 即Contanier.of
  • 拥有bind函数(即上面提到的bind, 而不是ES5的bind)

那么Promise具备了什么条件?

  • 拥有容器 Promise, 即上面第一点
  • Promise.resolve(value)将值转换为一个具有上下文的值, 即上面第二点。
  • Promise.prototype.then(onFullfill: value => Promise) 拥有一个bind(then)函数, 接受一个函数作为参数, 该函数接受一个普通值并返回一个含有上下文的值。 即上面第三点

不过Promise比Monad拥有更多的功能。

  • 如果then返回了一个正常的value, Promise会调用Promise.resolve将其转换为Promise
  • 普通的Monad只能提供在计算的时候传递一个值, 而Promise有两个不同的值 - 一个用于成功值,一个用于错误(类似于Either monad)。可以使用then方法的第二个回调或使用特殊的.catch方法捕获错误

Applicative Functor

提到了Functor和Monad而不提Applicative Functor就不完整了。

概念

Applicative Functor就是让不同 functor 可以相互应用(apply)的能力

举一个简单的例子, 假设有两个同类型的 functor,我们想把这两者作为一个函数的两个参数传递过去来调用这个函数。

  1. // 这样是行不通的,因为 2 和 3 都藏在瓶子里。
  2. add(Container.of(2), Container.of(3));
  3. //NaN
  4. // 使用可靠的 map 函数试试
  5. var container_of_add_2 = map(add, Container.of(2));
  6. // Container(add(2))

这时候我们创建了一个 Container,它内部的值是一个局部调用的(partially applied)的函数。确切点讲就是,我们想让 Container(add(2)) 中的 add(2) 应用到 Container(3) 中的 3 上来完成调用。也就是说,我们想把一个 functor 应用到另一个上。

巧的是,完成这种任务的工具已经存在了,即 chain 函数。我们可以先 chain 然后再 map 那个局部调用的 add(2),就像这样:

  1. Container.of(2).chain(function(two) {
  2. return Container.of(3).map(add(two));
  3. });

然而这样我们需要延迟Container.of(3)的建立, 这对我们来说是很不方便的也是没有必要的, 我们可以通过建立一个ap函数来达成我们想要的效果

  1. Container.prototype.ap = function(other_container) {
  2. return other_container.map(this.__value);
  3. }
  4. Container.of(2).map(add).ap(Container.of(3));
  5. // Container(5)

注意上面的add是科里化函数, this.__value是一个纯函数。

由于这种先 map 再 ap 的操作很普遍,我们可以抽象出一个工具函数 liftA2:

  1. const liftA2 = (f, m1, m2) => m1.map(f).ap(m2)
  2. liftA2(add, Container.of(2), Container.of(3))

应用

正如我们上面所说, 我们可以独立创建两个Container, 那么在Task中也可以同时发起两个http请求,而不必等到第一个返回再执行第二个

  1. // Http.get :: String -> Task Error HTML
  2. var renderPage = curry(function(destinations, events) { /* render page */ });
  3. Task.of(renderPage).ap(Http.get('/destinations')).ap(Http.get('/events'))
  4. // Task("<div>some page with dest and events</div>")

Functor\Monad\Applicative Functor的数学规律

Functor

  1. // identity
  2. map(id) === id;
  3. // composition
  4. compose(map(f), map(g)) === map(compose(f, g));

Monad

  1. bind(unit(x), f) f(x)
  2. bind(m, unit) m
  3. bind(bind(m, f), g) bind(m, x bind(f(x), g))

Applicative Functor

  1. Identity: A.of(x => x).ap(v) === v
  2. Homomorphism: A.of(f).ap(A.of(x)) === A.of(f(x))
  3. Interchange: u.ap(A.of(y)) === A.of(f => f(y)).ap(u)

js 与 函数式和面向对象

以下引用自文章漫谈 JS 函数式编程(一)

面向对象对数据进行抽象,将行为以对象方法的方式封装到数据实体内部,从而降低系统的耦合度。而函数式编程,选择对过程进行抽象,将数据以输入输出流的方式封装进过程内部,从而也降低系统的耦合度。两者虽是截然不同,然而在系统设计的目标上可以说是殊途同归的。

面向对象思想和函数式编程思想也是不矛盾的,因为一个庞大的系统,可能既要对数据进行抽象,又要对过程进行抽象,或者一个局部适合进行数据抽象,另一个局部适合进行过程抽象,这都是可能的。数据抽象不一定以对象实体为形式,同样过程抽象也不是说形式上必然是 functional 的,比如流式对象(InputStream、OutputStream)、Express 的 middleware,就带有明显的过程抽象的特征。但是在通常情况下,OOP更适合用来做数据抽象,FP更适合用来做过程抽象。

当然由于Javascript本身是多范式语言, 所以可以在合适的地方使用合适的编程方式。总而言之, 两者互不排斥,是可共存的。

尾递归优化

由于函数式编程,如果尾递归不做优化,很容易爆栈, 这个知识点有很多文章提出来了, 这里推荐一篇文章

声明式编程

声明式主要表现在于只关心结果而不关心过程, 这里推荐一篇轻松易懂的文章

或者举个例子:

在JQ时代的时候, 假如我们需要渲染一个DOM, 并改变其文字颜色, 我们需要这样的步骤:

  • 找到DOM的class或者id
  • 根据class或者id找到DOM
  • 重新赋值DOM的style属性的color属性

而在React中, 我们可以直接告诉JSX我们想要DOM的颜色变成红色即可。

  1. const textColor = 'red'
  2. const comp = () => {
  3. return (
  4. <div style={{
  5. color: textColor
  6. }} />
  7. )
  8. }

而关于声明式和函数式, 我个人认为函数式和声明式一样, 也是属于关心结果, 但是函数式最重要的特点是“函数第一位”,即函数可以出现在任何地方。 两者其实不应该做比较。

函数式编程在JS中的实践

  • Undescore/Lodash/Ramda库 特别是Lodash, 打开node_modules基本都能看到
  • Immutable-js 数据不可变
  • React
  • Redux
  • ES6 尾递归优化

函数式编程在前端开发中的优势

以下引用自知乎答案

优化绑定

说白了前端和后端不一样的关键点是后端HTTP较多,前端渲染多,前端真正的刚需是数据绑定机制。后端一次对话,计算好Response发回就完成任务了,所以后端吃了二十年年MVC老本还是挺好用的。前端处理的是连续的时间轴,并非一次对话,像后端那样赋值简单传递就容易断档,导致状态不一致,带来大量额外复杂度和Bug。不管是标准FRP还是Mobx这种命令式API的TFRP,内部都是基于函数式设计的。函数式重新发明的Return和分号是要比裸命令式好得多的(前端状态可以同步,后端线程安全等等,想怎么封装就怎么封装)。

封装作用

接上条,大幅简化异步,IO,渲染等作用/副作用相关代码。和很多人想象的不一样,函数式很擅长处理作用,只是多一层抽象,如果应用稍微复杂一点,这点成本很快就能找回来(Redux Saga是个例子,特别是你写测试的情况下)。渲染现在大家都可以理解幂等渲染地好处了,其实函数式编程各种作用和状态也是幂等的,对于复杂应用非常有帮助。

复用

引用透明,无副作用,代数设计让函数式代码可以正确优雅地复用。前端不像后端业务固定,做好业务分析和DDD就可以搭个静态结构,高枕无忧了。前端的好代码一定是活的,每处都可能乱改。可组合性其实很重要。通过高阶函数来组合效果和效率都要高于继承,试着多用ramda,你就可以发现绝大部分东西都能一行写完,最后给个实参就变成一个UI,来需求改两笔就变成另外一个。

总结

函数式编程在JS的未来是大放异彩还是泯然众人,都不影响我们学习它的思想。本文里面有许多引用没有特别指出,但都会在底部放上链接(如介意请留言), 望见谅。

参考&引用

声明式编程和命令式编程有什么区别?

用 JS 代码完整解释 Monad

怎么理解“声明式渲染”?

JavaScript函数式编程(二)

JavaScript Functors Explained

前端开发js函数式编程真实用途体现在哪里?

js 是更倾向于函数式编程了还是更倾向于面向对象?或者没有倾向?只是简单的提供了更多的语法糖?

漫谈 JS 函数式编程(一)

有哪些函数式编程在前端的实践经验?

前端使用面向对象式编程 还是 函数式编程 针对什么问题用什么方式 分别有什么具体案例?

什么是 Monad (Functional Programming)?

Monads In Javascript

Functor、Applicative 和 Monad

JavaScript 让 Monad 更简单

函数式编程

Js-函数式编程的更多相关文章

  1. js函数式编程术语总结 - 持续更新

    参考文档1 参考文档2 函数式编程术语 高阶函数 Higher-Order Functions 以函数为参数的函数 返回一个函数的函数 函数的元 Arity 比如,一个带有两个参数的函数被称为二元函数 ...

  2. js函数式编程——蹦床函数

    概述 这是我在学习函数式编程的时候,关于递归,尾递归,相互递归和蹦床函数的一些心得,记下来供以后开发时参考,相信对其他人也有用. 参考资料:JavaScript玩转Clojure大法之 - Tramp ...

  3. js函数式编程(二)-柯里化

    这节开始讲的例子都使用简单的TS来写,尽量做到和es6差别不大,正文如下 我们在编程中必然需要用到一些变量存储数据,供今后其他地方调用.而函数式编程有一个要领就是最好不要依赖外部变量(当然允许通过参数 ...

  4. js函数式编程(一)-纯函数

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

  5. JS函数式编程 - 概念

    最近在看Typescript,顺便看了一些函数式编程,然后半个国庆假期就没有了.做个笔记,分几个部分写吧. 最开始接触函数式编程的时候,第一个接触的概念就是高阶函数,和柯里化.咋一看,这不就是长期用来 ...

  6. js函数式编程

    最近在看朴灵的<深入浅出nodejs>其中讲到函数式编程.理解记录下 高阶函数 比较常见,即将函数作为参数,或是将函数作为返回值得函数. 如ECMAScript5中提供的一些数组方法 fo ...

  7. JS函数式编程【译】2.1 函数式编程语言

  8. JS函数式编程【译】2.2 与函数共舞

  9. JS函数式编程【译】2.3 函数式程序员的工具集

  10. JS函数式编程【译】第二章总结

随机推荐

  1. 正则表达式(Regular expressions)使用笔记

    Regular expressions are a powerful language for matching text patterns. This page gives a basic intr ...

  2. Windows上安装配置SSH教程(4)——WinSCP+OpenSSH 使用公钥自动登陆

    -------------------- 知识点汇总:http://www.cnblogs.com/feipeng8848/p/8559803.html -------------------- 重要 ...

  3. ISCC 2018(数字密文)

    做过iscc 2018之后有了很多的感触,也有更多的了解自己的不足之处,整理了一下web的wp, 为了保证各位小伙伴的阅读质量,我将会把wp以每一道题一个博文的形式写出来,希望能够帮助到你们 其中的步 ...

  4. 异步处理,Event Souring,事务补偿,实现最终一致性和服务的弹性和批处理

    这段时间一直学习极客时间皓哥的分布式架构,关于异步处理有一些感想用sketch做了一个图,展示上直观一些,和大家交流下

  5. 面试题之小炼牛刀zip,lambda,map

    # 现有两元祖,(('a'),('b')),(('c'),('d'))# 请使用python中匿名函数生成列表[{'a':'c'},{'b':'d'}]t1=(('a'),('b'))t2=(('c' ...

  6. CSS fixed 定位元素失效的问题

    一个示例 考察下面的代码: <head> <title>css filter issue</title> <style> body { height: ...

  7. 【推荐】.NETCore 简单且高级的库 csredis v3.0.0

    前言 .NETCore 从1.0发布历经坎坷,一开始各种库缺失到现在的部分完善,走到今天实属不易. 比如 redis-cli SDK 简直是坑出不穷. 过去 .net 最有名望的 ServiceSta ...

  8. 免费下载获取Odoo中文实施 应用 指南 手册

    引言 Odoo,以前叫OpenERP,是比利时Odoo S.A.公司开发的一个企业应用软件套件,开源套件包括一个企业应用快速开发平台,以及几千个Odoo及第三方开发的企业应用模块.Odoo适用于各种规 ...

  9. 5.App Inventor 2编程实例--指南针

    本视频来自:https://www.17coding.net 的  国庆特辑——指南针 共3个视频. 注意: 项目名字要使用英文. 项目完成后可以选择“打包APK”—“ 打包APK并下载到电脑”,然后 ...

  10. gitbook 入门教程之使用 gitbook-editor 编辑器开发电子书

    亲测,目前已不再支持旧版 gitbook-editor 编辑器,而官网也没有相应的新版编辑器,如果哪位找到了新版编辑器,还望告知! 现在注册 gitbook 账号会默认重定向到 新版官网,而 旧版官网 ...