再谈 JavaScript 函数式编程的适用性
栏目: JavaScript · 发布时间: 5年前
内容简介:最近在 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 在函数式编程实践中的局限。
总结如下:
-
一些 JS 函数式库,例如 Ramda, Sanctuary 和 crocks,可以帮助开发者使用 JS 进行函数式编程。crocks 的作者 evilsoft 在 egghead 上有一门课,讲用 State ADT 写 React 和 Redux 应用。课程中写的应用逻辑稍复杂,但 evilsoft 做到了纯 lambda 编程(全部用 expression,没有 statement)。当然这种实践只是一种 alternative,主要是用来学习思想。我觉得那种代码像清风一样。
-
用 JS 进行函数式编程也存在一些局限。维护门槛高是一方面。技术层面,用开源库去 polyfill 语言特性不是很可靠。Elm 和 PureScript 是更好的替代。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- C/C++语言的适用性
- Python 拓展之特殊函数(lambda 函数,map 函数,filter 函数,reduce 函数)
- Python 函数调用&定义函数&函数参数
- python基础教程:函数,函数,函数,重要的事说三遍
- C++函数中那些不可以被声明为虚函数的函数
- 017.Python函数匿名函数
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Effective C++
梅耶 (Scott Meyers) / 侯捷 / 电子工业出版社 / 2011-1-1 / 65.00元
《Effective C++:改善程序与设计的55个具体做法(第3版)(中文版)(双色)》内容简介:有人说C++程序员可以分为两类,读过Effective C++的和没读过的。世界项级C++大师scott Meyers成名之作的第三版的确当得起这样的评价。当您读过《Effective C++:改善程序与设计的55个具体做法(第3版)(中文版)(双色)》之后,就获得了迅速提升自己C++功力的一个契机......一起来看看 《Effective C++》 这本书的介绍吧!