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

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

几周前,我们讨论了使用 LiveScript 进行 JavaScript 函数式编程。从那时起,LiveScript 已经收到了很多更新(包括一些由 Hacker News 读者提出的功能),所以现在是时候发布第 2 部分了!

如果你还没有阅读第 1 部分,使用 LiveScript 和 prelude.ls 进行 JavaScript 函数式编程,我强烈建议你现在就阅读

简单回顾一下:LiveScript 是一种编译成 JavaScript 的语言。它只是 JavaScript,带有一些语法改进和功能添加,使函数式编程更容易。 prelude.ls 是它的标准库,基于 Haskell 的 Prelude 模块。

常量

函数式编程中的一个主要概念是不变的值。这意味着使用你无法重新赋值或修改的变量。这样做使代码更容易推理,因为你永远不必担心变量的状态——它们将永远只有一个值。这有助于减少代码中的错误数量。

JavaScript 目前不支持不变的值,但感谢 @satyr,LiveScript 现在有了常量!如果发现任何常量的重新赋值、增量或重新声明,编译时都会抛出错误。

它们使用 const 关键字定义

const x = 10
var x;
x = 10;

编译后的 JavaScript 没有什么不同,因为如上所述,JS 中目前还没有广泛支持常量。所有内容都在编译时进行检查。

在常量 x 定义之后,以下所有操作都会抛出错误。

x = 0  # error: redeclaration of constant "x" on line 2
x++    # error: increment of constant "x" on line 2
x += 2 # error: assignment to constant "x" on line 2

但是,属性修改仍然允许

const y = {name: 'amy'}
y.name = 'taylor'   # fine - results in y == {name: 'taylor'}

y = {name: 'hanna'} # not okay - error: redeclaration of constant "y" on line 4

对于那些来自具有不可变变量的语言并希望所有变量都为常量的用户来说,一直写出 const 可能很烦人。

编译时使用 -k--const 标志,使编译器将所有变量视为常量。例如

livescript -ck file.ls

部分应用

柯里化函数非常有用,但是当您处理的函数不是柯里化函数或参数顺序不符合您的预期时该怎么办?

您现在可以通过使用下划线 _ 作为占位符来部分应用函数,以表示您不想绑定的值。当您部分应用函数时,它不会执行该函数,而是返回一个新的函数,其中包含您已经绑定的参数,并且其参数是您放置占位符的参数。

filterNums = filter _, [1 to 5]

filterNums even  #=> [2, 4]
filterNums odd   #=> [1, 3, 5]
filterNums (< 3) #=> [1, 2]

# 'filter', 'even', and 'odd' are from prelude.ls
var filterNums, slice$ = [].slice;
filterNums = partialize$(filter, [void 8, [1, 2, 3, 4, 5]], [0]);
filterNums(even);
filterNums(odd);
filterNums((function(it){
  return it < 3;
}));
function partialize$(f, args, where){
  return function(){
    var params = slice$.call(arguments), i,
        len = params.length, wlen = where.length,
        ta = args ? args.concat() : [], tw = where ? where.concat() : [];
    for(i = 0; i < len; ++i) { ta[tw[0]] = params[i]; tw.shift(); }
    return len < wlen && len ? partialize$(f, ta, tw) : f.apply(this, ta);
  };
}

如果您调用部分应用的函数且没有参数,它将按原样执行而不是返回自身,允许您使用默认参数。

如果使用的函数参数顺序不好且未柯里化(例如 underscore.js),部分应用的函数对于管道也非常有用。

[1, 2, 3] |> _.map _, (* 2) |> _.reduce _, (+), 0 #=> 12
var slice$ = [].slice;
partialize$(_.reduce, [
  void 8, curry$(function(x$, y$){
    return x$ + y$;
  }), 0
], [0])(
partialize$(_.map, [
  void 8, (function(it){
    return it * 2;
  })
], [0])(
[1, 2, 3]));
function partialize$(f, args, where){
  return function(){
    var params = slice$.call(arguments), i,
        len = params.length, wlen = where.length,
        ta = args ? args.concat() : [], tw = where ? where.concat() : [];
    for(i = 0; i < len; ++i) { ta[tw[0]] = params[i]; tw.shift(); }
    return len < wlen && len ? partialize$(f, ta, tw) : f.apply(this, ta);
  };
}
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;
}

隐式访问和查找函数

此功能的想法来自 Hacker News 评论

在对对象集合进行映射和过滤时,能够以简洁的方式表达访问属性或调用方法的函数非常有用。

(.prop)

等价于

((it) -> it.prop)

以及

(obj.)

等价于

((it) -> obj[it])

一些例子

data = [{name: 'alice', age: 19},
        {name: 'tessa', age: 17}]

map (.name), data        #=> ['alice', 'tessa']

filter (.age > 18), data #=> [{name: 'alice', age: 19}] 

map (.toUpperCase!), ['hi', 'there'] #=> ['HI', 'THERE']



map (.join '-'), [[1, 2, 3], [4, 5]] #=> ['1-2-3', '4-5']



m4 = {blength: 15, color: 'black', sites: 'iron'}

map (m4.), ['color', 'sites'] #=> ['black', 'iron']
var data, m4;
data = [ { name: 'alice', age: 19 },
         { name: 'tessa', age: 17 } ];

map(function(it){ return it.name; }, data);

filter(function(it){ return it.age > 18; }, data);

map(function(it){
  return it.toUpperCase();
}, ['hi', 'there']);

map(function(it){
  return it.join('-');
}, [[1, 2, 3], [4, 5]]);

m4 = { blength: 15, color: 'black', sites: 'iron' };

map(function(it){
  return m4[it];
}, ['color', 'sites']);

包含 prelude.ls

我们一直在使用来自 prelude.ls 的函数式函数,例如 mapfilter 等。

每个文件中都必须导入 prelude.ls 可能会变得很烦人。因此,添加了一个编译器标志来自动导入 prelude.ls——只需使用 -d--prelude。例如

livescript -cd file.ls

这会将

if (typeof window != 'undefined' && window !== null) {
  prelude.installPrelude(window);
} else {
  require('prelude-ls').installPrelude(global);
}

添加到每个文件的顶部。installPrelude 函数会进行检查,以确保如果页面中包含多个文件,它只会安装一次。

参数简写

最近的另一个新增功能是访问函数参数的简写。

&0 是第一个参数,&1 是第二个参数,依此类推。

一个好的用例是在定义自定义函数以进行折叠时,因为这些函数需要两个参数并且不能利用其他一些可用的函数定义简写。

fold1 (-> &0 + &1 * &0), [1, 2, 3] 
#=> 12
fold1(function(){
  return arguments[0] + arguments[1] * arguments[0];
}, [1, 2, 3]);

解构

解构是从变量中提取值的强大方法。您可以将其赋值给数据结构而不是简单的变量,从而提取值。例如

[first, second] = [1, 2]
first  #=> 1
second #=> 2
var ref$, first, second;
ref$ = [1, 2], first = ref$[0], second = ref$[1];
first;
second;

您还可以使用散列

[head, ...tail] = [1 to 5]
head #=> 1
tail #=> [2, 3, 4, 5]



[first, ...middle, last] = [1 to 5]
first  #=> 1
middle #=> [2, 3, 4]
last   #=> 5
var ref$, head, tail, first, i$, middle, last, slice$ = [].slice;
ref$ = [1, 2, 3, 4, 5], head = ref$[0], tail = slice$.call(ref$, 1);
head;
tail;
ref$ = [1, 2, 3, 4, 5], first = ref$[0], middle = 1 < (i$ = ref$.length - 1) ? slice$.call(ref$, 1, i$) : (i$ = 1, []), last = ref$[i$];
first;
middle;
last;

...以及对象!

{name, age} = {weight: 110, name: 'emma', age: 20}
name #=> 'emma'
age  #=> 20
var ref$, name, age;
ref$ = {
  weight: 110,
  name: 'emma',
  age: 20
}, name = ref$.name, age = ref$.age;
name;
age;

您还可以使用 :label 为要解构的实体命名,以及任意嵌套解构。

[[x, ...xs]:list1, [y, ...ys]:list2] = [[1, 2, 3], [4, 5, 6]]
x     #=> 1
xs    #=> [2, 3]
list1 #=> [1, 2, 3]
y     #=> 4
ys    #=> [5, 6]
list2 #=> [4, 5, 6]
var ref$, list1, x, xs, list2, y, ys, slice$ = [].slice;
ref$ = [[1, 2, 3], [4, 5, 6]], list1 = ref$[0], x = list1[0], xs = slice$.call(list1, 1), list2 = ref$[1], y = list2[0], ys = slice$.call(list2, 1);
x;
xs;
list1;
y;
ys;
list2;

开关

在定义函数时,开关语句非常有用。它们允许您在函数主体其余部分之前干净地检查和返回边缘情况。

当您不切换任何内容时,则表示您正在切换 true,因此可以使用计算结果为布尔的表达式作为您的情况。

switch
case 5 == 6 
  'never'
case false
  'also never'
case 6 / 2 is 3
  'here'
switch (false) {
case 5 !== 6:
  'never';
  break;
case !false:
  'also never';
  break;
case 6 / 2 !== 3:
  'here';
}

(它编译成切换 false,因此它可以使用单个非 ! 而不是两个来转换情况。)

除了使用 case 关键字外,您还可以使用 | 表示 case,使用 => 表示 then。此外,如果您在箭头 ->、赋值 = 或冒号 : 后面跟着 case|,则表示隐式空开关语句。

此外,在 case 语句后面使用 otherwise_ 会编译成 default

state = | 2 + 2 == 5 => 'I love Big Brother'
        | _          => 'I love Julia'
var state;
state = (function(){
  switch (false) {
  case 2 + 2 !== 5:
    return 'I love Big Brother';
  default:
    return 'I love Julia';
  }
}());

非常适合函数定义

take(n, [x, ...xs]:list) =
  | n <= 0     => []
  | empty list => []
  | otherwise  => [x] +++ take n - 1, xs
var take, slice$ = [].slice;
take = curry$(function(n, list){
  var x, xs;
  x = list[0], xs = slice$.call(list, 1);
  switch (false) {
  case !(n <= 0):
    return [];
  case !empty(list):
    return [];
  default:
    return [x].concat(take(n - 1, xs));
  }
});
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;
}

管道更新

前向管道运算符 |> 现在具有更高的优先级,允许您将变量赋值给几个步骤的结果,而无需用括号括住整个内容。

result = 4 |> (+ 1) |> (* 2)
result #=> 10
var result;
result = (function(it){
  return it * 2;
})(
(function(it){
  return it + 1;
})(
4));
result;

您现在还可以部分应用管道运算符,并带有多个参数。

map (<| 4, 2), [(+), (*), (/)]
#=> [6, 8, 2]
map((function(it){
  return it(4, 2);
}), [
  curry$(function(x$, y$){ return x$ + y$; }),
  curry$(function(x$, y$){ return x$ * y$; }),
  curry$(function(x$, y$){ return x$ / y$;
  })
]);
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;
}

结论

这是对 LiveScript 中一些有助于函数式编程的功能的进一步概述。虽然我们专注于函数式编程,但 LiveScript 是一种多范式语言,并为命令式和面向对象编程提供了丰富的支持——包括类、mixin、super 等。

许多人已开始在他们的项目中使用 LiveScript——您可以查看 用 LiveScript 编写的项目 的部分列表,以及 支持 LiveScript 的项目

有任何想法或建议?您可以 在 GitHub 上创建工单 或在 LiveScript Google Group 中提出一般问题。

最后,有关更多信息,请查看 LiveScript 网站


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

Disqus 提供支持的评论