函数式编程能干什么(一)-- 写个烧脑异步控制流

栏目: 编程语言 · 发布时间: 6年前

内容简介:这次要写的小项目灵感源自月影老师的在这篇文章里我将会解释几个本次项目会用到的函数式编程概念,然后深入具体代码逐行解释。函数式编程的核心概念基本都来自范畴论,而范畴论确实比较难懂,所以我在解释时就尽量少涉及严谨理论,而是用灵活的类比来帮助大家理解。如果你去谷歌一些范畴论的概念,你能找到的定义大部分都过于学术化。比如你想知道 Applicative Functor 是什么,维基百科会告诉你那是介于 Functor 和 Monad 之间的数据类型。然后你又顺藤摸瓜去看什么是 Monad,到这一步你基本就没学下去

这次要写的小项目灵感源自月影老师的 漫谈 JS 函数式编程(一) 。月影老师在文章末尾演示了借助 Ramda 的 lift 函数,实现一个烧脑异步效果。几个月前读到月影老师的这篇文章时,我正学到 Applicative Functor,看到了 lift 函数,于是决定自己写一个,然后花了一个上午写出来了。

在这篇文章里我将会解释几个本次项目会用到的函数式编程概念,然后深入具体代码逐行解释。函数式编程的核心概念基本都来自范畴论,而范畴论确实比较难懂,所以我在解释时就尽量少涉及严谨理论,而是用灵活的类比来帮助大家理解。如果你去谷歌一些范畴论的概念,你能找到的定义大部分都过于学术化。比如你想知道 Applicative Functor 是什么,维基百科会告诉你那是介于 Functor 和 Monad 之间的数据类型。然后你又顺藤摸瓜去看什么是 Monad,到这一步你基本就没学下去的勇气了。

开始之前说明一下,我即将展示的代码真的很难懂,我尽量解释。我的前两篇发表在掘金的文章都有人批评不够接地气,感觉是在炫。我本意非如此,我开始学编程还没多久,我真的只是在通过写作的方式加深自己的理解。如果你觉得为了实现同样的效果,没必要写这么难懂的代码,欢迎给出你的方案。我是真不知道其它写法该怎么写,欢迎大家交流。

一,两个范畴论概念

1. Functor

我能想到的最简单定义:如果一个数据类型知道怎样把它的子元素映射到其它值,那它就是个 Functor 。说人话,比如给个数组 [3] ,我们可以把它的子元素映射到其它值: [3].map(x => x + 1) ,返回的结果是 [4] 。很显然,JS 里的数组就是个 Functor 。当然 Functor 有很多,数组只是大家最熟悉的一个。这篇文章里,为了避免引入不必要的复杂度,我就拿数组当做 Functor 来用。

2. Applicative Functor

可能是全网最简单的定义:Applicative Functor 就是由映射函数组成的 Functor。来看这个 Functor(从现在开始我们就不叫它数组了): const fns = [x => x + 1, x => x + 2, x => x + 3] ,所谓映射函数就是把一个值映射到另一个值的函数…… 那这个 Applicative Functor 是干嘛的呢?我们再创建个数组,哦不,Functor: const xs = [1, 2, 3] 。然后这样: fns.map(fn => xs.map(fn)) 。你可以理解为把第一个 Funcor 遍历一遍,然后把每一个函数当做第二个 Functor 的每一个元素的映射函数。啊,解释还是太累,看计算结果吧。上一步计算的结果是 [[2, 3, 4], [3, 4, 5] [4, 5, 6]]

二, lift 函数

我们刚刚其实已经定义了一个很重要的函数 ap , 它其实就是把 Applictive Functor 作用于其它 Functor 的函数: const ap = fns => xs => fns.map(f => xs.map(f)); 但这样是不够的,因为经过这次运算,我们得到的是一个二维数组,我们得把结果转成一维数组。先来定义个 工具 函数来把二维数组转成一维数组:

const flatten = arr => arr.reduce((flat, next) => flat.concat(next), []);
复制代码

最后的 ap 函数定义为:

const ap = fns => xs => flatten(fns.map(f => xs.map(f)));
复制代码

所谓 lift 就是把一个柯里化的函数提升到 Applicative Functor,然后再把这个 Applicative Functor 作用于其它 Functor。来看定义:

const lift = f => (head, ...tail) =>
  tail.reduce((fns, xs) => ap(fns)(xs), head.map(f));
复制代码

举个例子。先给 lift 传个函数 const add = x => y => x + y ,为了方便,我们只给 lift 传两个 Functor: const arr1 = [1, 2, 3, 4]; const arr2 = [4, 3, 2, 1] 然后 reduce 函数的第一步计算是 [1, 2, 3, 4].map(x => y => x + y) ,结果是 [y => y + 1, y => y + 2, y => y + 3, y => y + 4] ,接着,第二步计算是 ap([y => y + 1, y => y + 2, y => y + 3, y => y + 4])([4, 3, 2, 1]) ,计算结果是 [ 5, 4, 3, 2, 6, 5, 4, 3, 7, 6, 5, 4, 8, 7, 6, 5 ] 。你可以理解为把两个数组进行嵌套 for 循环然后把每次循环元素相加。

三,一些辅助函数

函数式编程最重要的部分是函数组合,在这部分我来解释我们将要用到的辅助函数。

partial:

function partial(func, argArr) {
  return function() {
    const allArguments = argArr.concat(Array.prototype.slice.call(arguments));
    return func.apply(this, allArguments);
  };
}
复制代码

Partial Application 也是函数式编程中比较重要的概念。它的意思是,给一个函数所需的部分参数,这个函数会接收这些参数,并返回一个新函数,等你给它传剩下的参数,等你把剩下的参数传进去,函数才会执行。如果你用 Haskell 等函数式编程语言,你是不需要这个函数的,因为 Haskell 默认所有函数都柯里化了。

chunk:

const chunk = (array, size) =>
  array.reduce((chunked, item) => {
    const last = chunked[chunked.length - 1];
    if (!last || last.length === size) {
        chunked.push([item]);
        return chunked;
    }
    last.push(item);
    return chunked;
  }, []);
复制代码

这个不用多解释了, chunk 函数会把一个一维数组转换成二维数组,目标二维数组的子数组长度可定制。

asyncPipe:

const asyncPipe = (...fns) => x => fns.reduce(async (y, f) => f(await y), x);
复制代码

如果你看过我另一篇文章 如何在 JS 代码中消灭 for 循环 ,你可能记得我写过一个 pipe 函数:

const pipe = (...fns) => (...args) => fns.reduce((fx, fy) => fy(fx), ...args);
复制代码

pipe 函数是非常重要的一个函数,它是用来函数组合的。给它传入多个函数,它会把传入的函数从左到右一个个执行,并把前一个函数的执行结果传给下一个函数。提个题外话,EcmaScript 可能会原生支持 pipe 操作符,它长这样 |> ,目前这个提案到了 stage 1 ,我非常希望它最终能进入语言标准。

asyncPipe 基本和 pipe 长一样,区别是它接受多个异步函数,逐个执行,当前函数执行完后,再把结果传给下一个函数,依次从左往右执行。

wait:

const wait = async ms => new Promise(resolve => setTimeout(resolve, ms));
复制代码

wait 函数传入毫秒数,它会让程序暂停指定毫秒时间,再继续执行函数。

四,开始写逻辑

  1. 首先我们要定义对指定某 DOM 节点,等待某段时间后,改变颜色这个行为:
const setColor = async (node, color) => {
  await wait(500);
  node.style.backgroundColor = color;
};
复制代码

这里我们等待 500ms 后再变色。

  1. 取到我们想变色的节点:
const items = Array.from(document.querySelectorAll("#list > li > span"));
复制代码
  1. 指定我们想变的颜色:
const color = ["red", "#ffa600", "#224de3", "#eee"];
复制代码
  1. 提供我们想要的目标属性组合:
const combine = node => color => [node, color];
复制代码
  1. 提供我们想要的行为组合:
let tasks = lift(combine)(items, color).map(comb => partial(setColor, comb));
复制代码

注意,上一步定义的任务组合,并没有区分不同灯,而我们目标是把每个灯变四个颜色,等待某段时间后,再开始下一个灯的变色。所以,我们需要把上一个任务组合拆成由多个子任务,每个子任务代表一盏灯,每盏灯变4次颜色:

const chunkedTasks = chunk(tasks, 4);
复制代码
  1. 然后,我们就能写主要逻辑了:
async function run(pause) {
  for (let i = 0; i < chunkedTasks.length; i++) {
    await asyncPipe(...chunkedTasks[i])();
    await wait(pause);
  }

  return run(pause);
}
复制代码
  1. 最后执行主逻辑,并传入每盏灯之间的等待时间:
run(1000);
复制代码
  1. 优化。主逻辑为了实现循环不停执行变色,使用了递归。为了避免递归爆栈,我们使用 trampoline 函数来避免每次递归都新增执行帧。
const trampoline = fn => (...args) => {
  let result = fn(...args);
  while (typeof result === "function") {
    result = result();
  }
  return result;
};

trampoline(run)(1000);
复制代码

至此,全部逻辑就写完了。

比较我这个版本的效果和原版的效果,不同在于:

  1. 没借助第三方库,全手写辅助函数。
  2. 每盏灯之间和每次变色之间的时间都能定制,而原版本中,每盏灯和每次变色之间的时间都是相同的。
  3. 所有灯变色完成后,回到第一盏灯,无限循环。

完整代码和效果在此处

四,更变态的需求

现在我们实现了比原版本要求更高的需求,但是到这里就完了吗?不不不,还有更有挑战的。如果我想要让每盏灯不同颜色之间的等待时间可随意定制,然后让每盏灯之间的等待时间也可随意定制,我们现在的代码结构能做到吗?现在没时间继续写,敬请期待……


以上所述就是小编给大家介绍的《函数式编程能干什么(一)-- 写个烧脑异步控制流》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

The Art and Science of Java

The Art and Science of Java

Eric Roberts / Addison-Wesley / 2007-3-1 / USD 121.60

In The Art and Science of Java, Stanford professor and well-known leader in CS Education Eric Roberts emphasizes the student-friendly exposition that led to the success of The Art and Science of C. By......一起来看看 《The Art and Science of Java》 这本书的介绍吧!

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

SHA 加密
SHA 加密

SHA 加密工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试