JavaScript 是一门很棒的语言,它对函数式编程风格提供了强大的支持。不幸的是,其能力被笨拙的语法和缺乏良好的标准库所掩盖。
LiveScript 是一种编译成 JavaScript 的语言。它只是 JavaScript 加了一些语法改进和功能添加,使函数式编程更容易。
prelude.ls 是一个 JavaScript 库(用 LiveScript 编写),它提供了一套丰富的函数来辅助函数式编程。它在某种程度上基于 Haskell 的 Prelude 模块。它是 LiveScript 推荐的基本库,但也可以与原生 JavaScript 一起使用。
什么是函数式编程风格?好吧,我们将讨论函数作为一等值、高阶函数、迭代、柯里化、组合、管道、运算符作为函数等等。
您熟悉 CoffeeScript 吗?如果是,您可以跳过几个部分。
但首先,我们将简要介绍 LiveScript 的基础知识
像许多现代语言一样,代码块由空格缩进分隔,换行符用于代替分号来终止语句(如果要在一行上写多个语句,您仍然可以使用分号)。
例如(左侧为 LiveScript,右侧为编译后的 JavaScript)
if 2 + 2 == 4 doSomething()
if (2 + 2 === 4) { doSomething(); }
您可以使用 LiveScript 网站 上的 LiveScript 编译器/REPL 自己尝试所有这些示例。
为了进一步澄清,在调用函数时可以省略括号。
add 2, 3
add(2, 3);
注释是
# from here to the end of the line.
// from here to the end of the line.
Lisp 黑客们,您可能会很高兴地知道,您可以在变量和函数的名称中使用连字符。这些名称等同于驼峰命名法,并被编译成驼峰命名法。例如,my-value = 42
== myValue = 42
。
定义函数(我们在函数式编程中经常这样做)在 JavaScript 中可能很麻烦,因为您必须使用一个八个字母的关键字 function
,并且要有所有的大括号和分号。
在 LiveScript 中,事情变得轻松了许多
(x, y) -> x + y -> # an empty function times = (x, y) -> x * y # multiple lines, and be assigned to a var like in JavaScript
var times; (function(x, y){ return x + y; }); (function(){}); times = function(x, y){ return x * y; };
如您所见,函数定义要短得多!您可能还注意到我们省略了 return
。在 LiveScript 中,几乎所有内容都是表达式,并且自动返回最后一个达到的表达式。但是,如果需要,您仍然可以使用 return
强制返回,并且可以向定义中添加感叹号以禁止自动返回 noRet = !(x) -> ...
。
与 JavaScript 一样,LiveScript 中的函数是一等值。这意味着它们可以在执行时创建、存储在数据结构中、作为参数传递以及从函数中返回。将函数作为参数的函数是高阶函数。
addTwo = (x) -> x + 2 wrap = (f, x) -> # take a function as an argument (y) -> x + f(y) + x # returns a function dollarAddTwo = wrap addTwo, '$' dollarAddTwo 3 #=> '$5$'
var addTwo, wrap, dollarAddTwo; addTwo = function(x){ return x + 2; }; wrap = function(f, x){ return function(y){ return x + f(y) + x; }; }; dollarAddTwo = wrap(addTwo, '$'); dollarAddTwo(3);
编程通常涉及处理项目集合,无论是列表、对象还是字符串。函数式编程允许您以强大的方式处理集合。这里我们将使用来自prelude.ls 的函数。
首先,假设您想获取一个集合并对每个元素执行某些操作——这将需要一个 map
。它的第一个参数是将应用于每个元素的函数。它返回一个您输入类型的新集合。因此,列表将返回一个新列表,对象将返回一个新对象。
map ((x) -> x + 2), [1, 2, 3] #=> [3, 4, 5]
map(function(x){ return x + 2; }, [1, 2, 3]);
您可能希望获取集合中的所有项目并从中获取一个单一的值——这需要一个 fold
。第一个参数是一个具有两个参数的函数,它使用此函数将列表组合成一个单一的值。第二个参数是要开始使用的值。
fold ((x, y) -> x + y), 0, [1, 2, 3] #=> 6
fold(function(x, y){ return x + y; }, 0, [1, 2, 3]);
或者您可能想要过滤掉一些项目——这需要恰如其分地命名为 filter
。第一个参数是一个函数,它将应用于列表的每个项目。如果该函数计算结果为真,则它会将该项目添加到结果集合中。
filter even, {a: 1, b: 2, c: 3, d: 4} #=> {b: 2, d: 4}
filter(even, { a: 1, b: 2, c: 3, d: 4 });
还有许多其他可用的函数,只需查看prelude.ls 网站 以获取所有函数的文档。
对于这些高阶函数,拥有一个简洁地创建要使用的函数的方法非常有用。我们将在下面探讨几个选项。首先,您可以使用柯里化函数来创建您已定义的函数的子集函数。您可以使用组合来创建由您已定义的几个其他函数组合而成的函数,还可以使用运算符作为函数。
柯里化函数非常强大。从本质上讲,当使用比定义时更少的参数调用时,它们会返回一个部分应用的函数。这意味着它返回一个函数,其参数是您未提供的参数,并且已经绑定了您提供的参数的值。它们在 LiveScript 中使用长箭头定义。也许一个例子会让事情更清楚
times = (x, y) --> x * y times 2, 3 #=> 6 (normal use works as expected) double = times 2 double 5 #=> 10
var times, double; times = curry$(function(x, y){ return x * y; }); times(2, 3); double = times(2); double(5); function curry$(f, args){ return f.length > 1 ? function(){ var params = args ? args.concat() : []; return params.push.apply(params, arguments) < f.length && arguments.length ? curry$.call(this, f, params) : f.apply(this, params); } : f; }
柯里化允许您在高阶集合函数中保持简洁。假设我们没有定义 double
。
map (times 2), [1, 2, 3] #=> [2, 4, 6]
map(times(2), [1, 2, 3]);
柯里化使定义函数作为您已定义函数的行为的子集变得非常容易。但是,如果您想将多个函数的行为组合在一起怎么办?
组合允许您通过将函数“组合”成一系列函数来创建函数。LiveScript 有两个用于组合的运算符,向前 >>
和向后 <<
。
(f << g) x
等效于 f(g(x))
,而 (f >> g) x
等效于 g(f(x))
。例如
even = (x) -> x % 2 == 0 invert = (x) -> not x odd = invert << even
var even, invert, odd; even = function(x){ return x % 2 === 0; }; invert = function(x){ return !x; }; odd = compose$([invert, even]); function compose$(fs){ return function(){ var i, args = arguments; for (i = fs.length; i > 0; --i) { args = [fs[i-1].apply(this, args)]; } return args[0]; }; }
假设 odd
未定义,我们如何在不定义新函数的情况下轻松过滤列表?
filter (invert << even), [1, 2, 3, 4, 5] #=> [1, 3, 5]
filter(compose$([invert, even]), [1, 2, 3, 4, 5]); function compose$(fs){ return function(){ var i, args = arguments; for (i = fs.length; i > 0; --i) { args = [fs[i-1].apply(this, args)]; } return args[0]; }; }
是的,F# 用户,它就像 F#!LiveScript 还包括您喜欢的管道运算符——稍后我们将详细介绍。
Haskell 程序员可以使用空格点代替 <<
,例如 f . g
。
为非常基本的操作定义函数似乎很乏味,有没有更好的方法?
您可以将运算符用作函数,只需将它们括在括号中即可。您还可以部分应用它们的参数!
timesTwo = (* 2) timesTwo 4 #=> 8 odd = (not) << even
var timesTwo, odd; timesTwo = (function(it){ return it * 2; }); timesTwo(4); odd = compose$([not$, even]); function compose$(fs){ return function(){ var i, args = arguments; for (i = fs.length; i > 0; --i) { args = [fs[i-1].apply(this, args)]; } return args[0]; }; } function not$(x){ return !x; }
所有这些都很好,但是如果您的流程中有多个步骤怎么办?
您可以将值传入管道中,而不是一系列嵌套函数调用
[1, 2, 3, 4, 5] |> filter even |> map (* 2) |> fold (+), 0 #=> 12
fold(curry$(function(x$, y$){ return x$ + y$; }), 0)( map((function(it){ return it * 2; }))( filter(even)( [1, 2, 3, 4, 5]))); function curry$(f, args){ return f.length > 1 ? function(){ var params = args ? args.concat() : []; return params.push.apply(params, arguments) < f.length && arguments.length ? curry$.call(this, f, params) : f.apply(this, params); } : f; }
但是,如果您想要的函数无法通过柯里化函数的部分应用、函数组合或运算符的使用来创建呢?
如果您要定义的函数只有一个参数,那么您可以省略 (x) ->
部分,而只使用 it
作为隐式参数。
filter (-> it.length > 5), ['tiny', 'middle', 'longer'] #=> ['middle', 'longer']
filter(function(it){ return it.length > 5; }, ['tiny', 'middle', 'longer']);
您可以像运算符一样使用函数,如果需要,甚至可以部分应用其第二个参数——只需将它们括在反引号中即可。
startsWith = (xs, x) -> x == xs.slice 0, 1 'hello' `startsWith` 'h' #=> true
var startsWith; startsWith = function(xs, x){ return x === xs.slice(0, 1); }; startsWith('hello', 'h');
厌倦了看到 map
和 filter
?或者想一次处理多个集合?您可以使用列表推导式和对象推导式。
r = [x + 1 for x in [2, 3, 4, 5] when x + 1 isnt 4] r #=> [3, 5, 6] r = {[key, val * 2] for key, val of {a: 1, b: 2}} r #=> {a: 2, b: 4} r = [x + y for x in [1, 2] for y in ['a', 'b']] r #=> ['1a', '1b', '2a', '2b']
var res$, i$, ref$, len$, x, r, key, val, j$, ref1$, len1$, y; res$ = []; for (i$ = 0, len$ = (ref$ = [2, 3, 4, 5]).length; i$ < len$; ++i$) { x = ref$[i$]; if (x + 1 !== 4) { res$.push(x + 1); } } r = res$; r; res$ = {}; for (key in ref$ = { a: 1, b: 2 }) { val = ref$[key]; res$[key] = val * 2; } r = res$; r; res$ = []; for (i$ = 0, len$ = (ref$ = [1, 2]).length; i$ < len$; ++i$) { x = ref$[i$]; for (j$ = 0, len1$ = (ref1$ = ['a', 'b']).length; j$ < len1$; ++j$) { y = ref1$[j$]; res$.push(x + y); } } r = res$; r;
您可以使用连接运算符 +++
来连接两个列表,或将某些内容添加到列表的末尾。
[1, 2] +++ [3, 4] #=> [1, 2, 3, 4] [1, 2] +++ 3 #=> [1, 2, 3]
[1, 2].concat([3, 4]); [1, 2].concat(3);
请注意,它返回一个新列表,并且不会修改输入的列表。
这是一些在 LiveScript 和 prelude.ls 中可用的功能的简要概述,以辅助函数式编程。两者都具有更多强大而有用的功能——为了让您一窥究竟
take = (n, [x, ...xs]:list) --> | n <= 0 => [] | empty list => [] | otherwise => [x] +++ (take n - 1, xs) take 2, [1 to 5] #=> [1, 2]
查看他们的网站了解更多信息:LiveScript 和 prelude.ls。
在 Hacker News、r/programming 和 r/javascript 上关注讨论。
阅读使用 LiveScript 进行 JavaScript 函数式编程 - 第 2 部分。
有关 LiveScript 和 prelude.ls 的更多信息,请关注 @gkzahariev。
评论由 Disqus 提供支持