Home  >  Article  >  Web Front-end  >  JavaScript functional programming (3)

JavaScript functional programming (3)

黄舟
黄舟Original
2017-03-06 14:02:541076browse

In the second article, we introduced several common Functors such as Maybe, Either, IO, maybe Many people who have read the second article will have doubts:

"What is the use of these things?" 』

In fact, if you just want to learn to write functional code with small side effects, reading the first article is enough. The second article and the third one here focus on some functional theory practices, and yes, these are difficult (but not impossible) to apply in actual production, because many wheels have already been built and are very easy to implement. It works. For example, the asynchronous calling specification of Promise, which is currently used on a large scale on the front end, is actually a kind of Monad (will be discussed later); the increasingly mature Redux is implemented as a variant of Flux, and the core concept is also State machines and functional programming.

1. Monad

There are endless introductions and tutorials about Monad on the Internet. Many articles are better than mine below, so I just use a simpler one here. Monad is introduced in an easy-to-understand way. Of course, the disadvantage of being easy-to-understand is that it is not rigorous, so forgive me/w\

If you understand the specification of Promise, you should remember an amazing feature of Promise. :

doSomething()
    .then(result => {
        // 你可以return一个Promise链!
        return fetch('url').then(result => parseBody(result));
    })
    .then(result => {
        // 这里的result是上面那个Promise的终值
    })

doSomething()
    .then(result => {
        // 也可以直接return一个具体的值!
        return 123;
    })
    .then(result => {
        // result === 123
    })


For a callback function of Promise, it can either directly return a value or return a new Promise, but for their subsequent callback functions Said that the two are equivalent, which cleverly solves the long-criticized nesting hell in nodejs.

In fact, Promise is a kind of Monad. Yes, you may have to write a lot of Promises every day, but you didn't know until now that this thing you use every day is actually a functional concept that sounds very high-level.

Let's actually implement a Monad. If you don't want to read it, just remember "Promise is a kind of Monad" and just skip this chapter.

Let’s write a function cat. The function of this function is the same as cat under the Linux command line. It reads a file and then prints the contents of the file. For the implementation of IO here, please refer to the previous article:

import fs from 'fs';
import _ from 'lodash';

var map = _.curry((f, x) => x.map(f));
var compose = _.flowRight;

var readFile = function(filename) {
    return new IO(_ => fs.readFileSync(filename, 'utf-8'));
};

var print = function(x) {
    return new IO(_ => {
        console.log(x);
        return x;
    });
}

var cat = compose(map(print), readFile);

cat("file")
//=> IO(IO("file的内容"))

Since there are two IOs involved here: reading files and printing, the final result is that we get two layers of IO, think To run it, you can only call:

cat("file").__value().__value();
//=> 读取文件并打印到控制台

It’s embarrassing, right? If we involve 100 IO operations, then do we have to write 100 __value( ) ?

Of course it can’t be inelegant. Let’s implement a join method. Its function is to peel off a layer of Functor and expose the contents to us:

var join = x => x.join();
IO.prototype.join = function() {
  return this.__value ? IO.of(null) : this.__value();
}

// 试试看
var foo = IO.of(IO.of('123'));

foo.join();
//=> IO('123')

After having the join method, Just a little more elegant:

var cat = compose(join, map(print), readFile);
cat("file").__value();
//=> 读取文件并打印到控制台


join method can flatten the Functor ( flatten), we generally call a Functor with this ability a Monad.

This is just a very simple removal of a layer of Functor packaging, but as elegant programmers, we cannot always call # manually after map ##join to strip off redundant packaging, otherwise the code will look like this:

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

So we need a method called chain to achieve the chain call we expect, which will be called after map Automatically calling join to remove redundant packaging is also a major feature of Monad:

var chain = _.curry((f, functor) => functor.chain(f));
IO.prototype.chain = function(f) {
  return this.map(f).join();
}

// 现在可以这样调用了
var doSomething = compose(chain(f), chain(g), chain(h));

// 当然,也可以这样
someMonad.chain(f).chain(g).chain(h)

// 写成这样是不是很熟悉呢?
readFile('file')
    .chain(x => new IO(_ => {
        console.log(x);
        return x;
    }))
    .chain(x => new IO(_ => {
        // 对x做一些事情,然后返回
    }))


Haha, you may see Comes out, isn't

chain similar to then in Promise? Yes, they are indeed consistent in behavior (then will have a little more logic, it will record the number of nesting levels and distinguish between Promise and ordinary return values), and Promise is indeed a functional Thought.

(I originally wanted to use Promise as an example to write some examples below, but I guess everyone who can see here should be able to write various Promise chains proficiently, so I won’t write 0w0)

In short, Monad allows us to avoid nesting hell and can easily perform deeply nested functional programming, such as IO and other asynchronous tasks.

2. Application of functional programming

Okay, this is the introduction of some basic theories of functional programming. If you want to know more, it is actually recommended to learn Haskell or Lisp is a more orthodox functional language. Let’s answer a question: What is the use of functional programming in practical applications?

1. React

React can be seen everywhere now. If you want to ask why it is popular, some people may say that it has "good performance", "cool", " "Rich third-party components", "novel", etc., but these are not the most critical. The most important thing is that React brings new concepts to front-end development: functional and state machines.

Let’s take a look at how to write a “pure component” in React:

var Text = props => (
    <p style={props.style}>{props.text}</p>
)

咦这不就是纯函数吗?对于任意的 text 输入,都会产生唯一的固定输出,只不过这个输出是一个 virtual DOM 的元素罢了。配合状态机,就大大简化了前端开发的复杂度:

state => virtual DOM => 真实 DOM

在 Redux 中更是可以把核心逻辑抽象成一个纯函数 reducer:

reducer(currentState, action) => newState


关于 React+Redux(或者其它FLUX架构)就不在这里介绍太多了,有兴趣的可以参考相关的教程。

2、Rxjs

Rxjs 从诞生以来一直都不温不火,但它函数响应式编程(Functional Reactive Programming,FRP)的理念非常先进,虽然或许对于大部分应用环境来说,外部输入事件并不是太频繁,并不需要引入一个如此庞大的 FRP 体系,但我们也可以了解一下它有哪些优秀的特性。

在 Rxjs 中,所有的外部输入(用户输入、网络请求等等)都被视作一种 『事件流』:

--- 用户点击了按钮 --> 网络请求成功 --> 用户键盘输入 --> 某个定时事件发生 --> ......

举个最简单的例子,下面这段代码会监听点击事件,每 2 次点击事件产生一次事件响应:

var clicks = Rx.Observable
    .fromEvent(document, &#39;click&#39;)
    .bufferCount(2)
    .subscribe(x => console.log(x)); // 打印出前2次点击事件


其中 bufferCount 对于事件流的作用是这样的:

是不是很神奇呢?Rxjs 非常适合游戏、编辑器这种外部输入极多的应用,比如有的游戏可能有『搓大招』这个功能,即监听用户一系列连续的键盘、鼠标输入,比如上上下下左右左右BABA,不用事件流的思想的话,实现会非常困难且不优雅,但用 Rxjs 的话,就只是维护一个定长队列的问题而已:

var inputs = [];
var clicks = Rx.Observable
    .fromEvent(document, &#39;keydown&#39;)
    .scan((acc, cur) => {
        acc.push(cur.keyCode);
        var start = acc.length - 12 < 0 ? 0 : acc.length - 12;
        return acc.slice(start);
    }, inputs)
    .filter(x => x.join(&#39;,&#39;) == [38, 38, 40, 40, 37, 39, 37, 39, 66, 65, 66, 65].join(&#39;,&#39;))// 上上下下左右左右BABA,这里用了比较奇技淫巧的数组对比方法
    .subscribe(x => console.log(&#39;!!!!!!ACE!!!!!!&#39;));


当然,Rxjs 的作用远不止于此,但可以从这个范例里看出函数响应式编程的一些优良的特性。

3、Cycle.js

Cycle.js 是一个基于 Rxjs 的框架,它是一个彻彻底底的 FRP 理念的框架,和 React 一样支持 virtual DOM、JSX 语法,但现在似乎还没有看到大型的应用经验。

本质的讲,它就是在 Rxjs 的基础上加入了对 virtual DOM、容器和组件的支持,比如下面就是一个简单的『开关』按钮:

import xs from &#39;xstream&#39;;
import {run} from &#39;@cycle/xstream-run&#39;;
import {makeDOMDriver} from &#39;@cycle/dom&#39;;
import {html} from &#39;snabbdom-jsx&#39;;

function main(sources) {
  const sinks = {
    DOM: sources.DOM.select(&#39;input&#39;).events(&#39;click&#39;)
      .map(ev => ev.target.checked)
      .startWith(false)
      .map(toggled =>
        <p>
          <input type="checkbox" /> Toggle me
          <p>{toggled ? &#39;ON&#39; : &#39;off&#39;}</p>
        </p>
      )
  };
  return sinks;
}

const drivers = {
  DOM: makeDOMDriver(&#39;#app&#39;)
};

run(main, drivers);


当然,Cycle.js 这种『侵入式』的框架适用性不是太广,因为使用它就意味着应用中必须全部或者大部分都要围绕它的理念设计,这对于大规模应用来说反而是负担。

三、总结

既然是完结篇,那我们来总结一下这三篇文章究竟讲了些啥?

第一篇文章里,介绍了纯函数、柯里化、Point Free、声明式代码和命令式代码的区别,你可能忘记得差不多了,但只要记住『函数对于外部状态的依赖是造成系统复杂性大大提高的主要原因』以及『让函数尽可能地纯净』就行了。

第二篇文章,或许是最没有也或许是最有干货的一篇,里面介绍了『容器』的概念和 MaybeEitherIO 这三个强大的 Functor。是的,大多数人或许都没有机会在生产环境中自己去实现这样的玩具级 Functor,但通过了解它们的特性会让你产生对于函数式编程的意识。

软件工程上讲『没有银弹』,函数式编程同样也不是万能的,它与烂大街的 OOP 一样,只是一种编程范式而已。很多实际应用中是很难用函数式去表达的,选择 OOP 亦或是其它编程范式或许会更简单。但我们要注意到函数式编程的核心理念,如果说 OOP 降低复杂度是靠良好的封装、继承、多态以及接口定义的话,那么函数式编程就是通过纯函数以及它们的组合、柯里化、Functor 等技术来降低系统复杂度,而 React、Rxjs、Cycle.js 正是这种理念的代言人,这可能是大势所趋,也或许是昙花一现,但不妨碍我们去多掌握一种编程范式嘛0w0

以上就是JavaScript函数式编程(三)的内容,更多相关内容请关注PHP中文网(www.php.cn)!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn