再谈 JavaScript 函数式编程的适用性

栏目: JavaScript · 发布时间: 7年前

内容简介:最近在 Udemy 上学 Stephen Grider 的课程 Machine Learning With JavaScript。由于是个人业余练习,课程中的代码我都用纯函数式编写。其中有一部分要解决这个问题:给定一个矩阵数据,例如要求把矩阵的每列进行数据 normalization,就是说基于每列数据的最大数和最小数,将该列数据转换成从 0 到 1 的小数。如为了不改变原数据,上面的函数在进行操作前,用 lodash 对数据进行了深拷贝。

最近在 Udemy 上学 Stephen Grider 的课程 Machine Learning With JavaScript。由于是个人业余练习,课程中的代码我都用纯函数式编写。其中有一部分要解决这个问题:给定一个矩阵数据,例如

const data = [
  [12, 2, 5, 4],
  [13, 6, 3, 5],
  [17, 2, 5, 4],
  [14, 9, 3, 4],
  [15, 9, 3, 4]
];
复制代码

要求把矩阵的每列进行数据 normalization,就是说基于每列数据的最大数和最小数,将该列数据转换成从 0 到 1 的小数。如 [1, 2, 3] 转换成 [0, 0.5, 1] 。另外要求操作列数可定制。课程给的答案如下:

function normalizeMatrix(range, data) {
  const copy = _.cloneDeep(data);
  // 只在给定的列数范围内操作
  for (let i = 0; i < range; i++) {
    const col = copy.map(row => row[i]);
    const max = _.max(col);
    const min = _.min(col);
    for (let j = 0; j < copy.length; j++) {
      copy[j][i] = (copy[j][i] - min) / (max - min);
    }
  }
  return copy;
}
复制代码

为了不改变原数据,上面的函数在进行操作前,用 lodash 对数据进行了深拷贝。

我使用 Ramda 写出的结果如下:

// Ramda 没有 min 和 max 辅助函数,我用自己写的
const min = list => Math.min(...list);

const max = list => Math.max(...list);

const applyMinMax = R.curry((min, max, list) =>
  list.map(num => (num - min) / (max - min))
);

const normalizeRow = R.converge(applyMinMax, [min, max, R.identity]);

const applyCalc = limit => list =>
  list.map((row, idx) => (idx >= limit ? row : normalizeRow(row)));

const normalizeMatrix = range =>
  R.compose(
    R.transpose,
    applyCalc(range),
    R.transpose
  )
复制代码

我写的这个版本,先用 transpose 函数把原矩阵进行行列置换,数据操作完成后,再置换回原形状。

看上去两个版本都很别扭。第一个把数据进行了深拷贝,第二个把数据行列置换了两次。那性能比较如何?

我的电脑测试结果如下:

const getSample = length =>
  Array.from({ length }, _ =>
    Array.from({ length }, _ => Math.floor(Math.random() * 100))
  );

const sampleData = getSample(1000)

// 第一个版本
// => ​​​​​imperative: 255.112ms​​​​​
console.time('imperative')
normalizeMatrix1(1000, sampleData)
console.timeEnd('imperative')

// 第二个版本
// => ramda: 177.802ms​​​​​
console.time('ramda')
normalizeMatrix2(1000)(sampleData)
console.timeEnd('ramda')
复制代码

Ramda 版本性能更优。

基于这个例子我有下面这些思考:

一,指令式编程在某些上下文有其适用性。甚至大多数时候,主流的实践都偏好指令式代码。写指令式代码目的有两个:一是考虑性能。指令式代码对过程控制比较细粒度,很容易优化性能。二是大多数语言对于 lambda 表达式的支持,不管是语言层面的,还是生态层面的,都不是很好,所以只能用指令式写。但上面的例子说明了,某些情况下,按照过程式的定势思维写出的代码,不一定能达到目的。

二,即使是高阶语言的指令式代码,其实在函数式编程上下文里面也相当于汇编指令。比如,上面用到的 transpose 函数,其实是用两层嵌套 while 循环实现的,实现细节里面也有用到临时变量等指令式元素。而这些实施细节是隐藏不见的,对于函数使用者来说,把实施细节当做汇编指令是没多大问题的。

上面第二点,可以参考 Haskell 继续说明下。

经典的快排算法,用 JS,即使用递归来写,也要很多步骤:

const quickSort = list => {
  if (list.length === 0) return list;
  const [pivot, ...rest] = list;
  const smaller = [];
  const bigger = [];
  rest.forEach(x => (x < pivot ? smaller.push(x) : bigger.push(x)));

  return [...quickSort(smaller), pivot, ...quickSort(bigger)];
};
复制代码

Haskell 版本:

quicksort     [] = []
quicksort (x:xs) = quicksort smaller ++ [x] ++ quicksort larger
                    where 
                        smaller = [a | a <- xs, a <= x]
                        larger  = [b | b <- xs, b > x]
复制代码

由于 Haskell 语言层面支持惰性求值,递归,和 list comprehension,所以它天然支持高表达性语法,至于底层实现和优化则交给编译器去处理,编写者不用关心。而像 JavaScript,由于语言层面没有 Haskell 的这些特性,所以需要某些库,用指令式的方式实现某些 lambda 功能。用库去解决本该由编译器去解决的问题肯定不是最优的,这是 JavaScript 在函数式编程实践中的局限。

总结如下:

  1. 一些 JS 函数式库,例如 Ramda, Sanctuary 和 crocks,可以帮助开发者使用 JS 进行函数式编程。crocks 的作者 evilsoft 在 egghead 上有一门课,讲用 State ADT 写 React 和 Redux 应用。课程中写的应用逻辑稍复杂,但 evilsoft 做到了纯 lambda 编程(全部用 expression,没有 statement)。当然这种实践只是一种 alternative,主要是用来学习思想。我觉得那种代码像清风一样。

  2. 用 JS 进行函数式编程也存在一些局限。维护门槛高是一方面。技术层面,用开源库去 polyfill 语言特性不是很可靠。Elm 和 PureScript 是更好的替代。


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

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

Introduction to Tornado

Introduction to Tornado

Michael Dory、Adam Parrish、Brendan Berg / O'Reilly Media / 2012-3-28 / USD 23.99

Tornado is a scalable, non-blocking web server and web application framework written in Python. It is also light-weight to deploy, fun to write for, and incredibly powerful. Tornado was written with p......一起来看看 《Introduction to Tornado》 这本书的介绍吧!

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

图片转BASE64编码
图片转BASE64编码

在线图片转Base64编码工具

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换