使用 LiveScript 和 prelude.ls 在 JavaScript 中进行函数式编程

2012年6月19日,更新于2012年7月30日 - George Zahariev

JavaScript 是一门很棒的语言,它对函数式编程风格提供了强大的支持。不幸的是,其能力被笨拙的语法和缺乏良好的标准库所掩盖。

LiveScript 是一种编译成 JavaScript 的语言。它只是 JavaScript 加了一些语法改进和功能添加,使函数式编程更容易。

prelude.ls 是一个 JavaScript 库(用 LiveScript 编写),它提供了一套丰富的函数来辅助函数式编程。它在某种程度上基于 Haskell 的 Prelude 模块。它是 LiveScript 推荐的基本库,但也可以与原生 JavaScript 一起使用。

什么是函数式编程风格?好吧,我们将讨论函数作为一等值、高阶函数、迭代、柯里化、组合、管道、运算符作为函数等等。

您熟悉 CoffeeScript 吗?如果是,您可以跳过几个部分

但首先,我们将简要介绍 LiveScript 的基础知识

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');

推导式

厌倦了看到 mapfilter?或者想一次处理多个集合?您可以使用列表推导式和对象推导式。



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);

请注意,它返回一个新列表,并且不会修改输入的列表。

结论

这是一些在 LiveScriptprelude.ls 中可用的功能的简要概述,以辅助函数式编程。两者都具有更多强大而有用的功能——为了让您一窥究竟

take = (n, [x, ...xs]:list) -->
  | n <= 0     => []
  | empty list => []
  | otherwise  => [x] +++ (take n - 1, xs)

take 2, [1 to 5] #=> [1, 2]

查看他们的网站了解更多信息:LiveScriptprelude.ls

Hacker Newsr/programmingr/javascript 上关注讨论。

阅读使用 LiveScript 进行 JavaScript 函数式编程 - 第 2 部分


有关 LiveScript 和 prelude.ls 的更多信息,请

评论由 Disqus 提供支持