12 December 2011

for (int i = 0; i < LENGTH; ++i) {
    print(i);
}

无论是C++, JAVA还是javascript, 这种for循环都是非常常见的代码; 但是在所有的函数式语言中: Scala, Scheme, Erlang, Haskell为什么语法中都没有这样的for循环?

考察下面这段代码:

var a = ['a.html', 'b.html', 'c.html'];
for (var i = 0; i < a.length; ++i) {
    console.log(a[i] + ": " + a[i].length);
}

运行起来还算正常

现在修改一下需求, 返回这几个页面的字符数:

var a = ['a.html', 'b.html', 'c.html'];
for (var i = 0; i < a.length; ++i) {
    $.ajax(a[i], {
        success: function (text) {
            console.log(a[i] + ": " + text.length);
        }
    });
}

看起来这段代码与上面那段没什么太大区别, 应该不会出问题, 但是实际应用中, 立刻就看到问题了, 这段代码输出:

undefined: 100
undefined: 200
undefined: 300

原因很简单, 虽然闭包success捕获了变量i, 但是i在后面发生了变化, 调用success闭包的时候, i变成了3, a[i]返回undefined.

解决这个问题的方法也很简单: 不要使用变量

var a = ['a.html', 'b.html', 'c.html'];
$.each(a, function (k, v) {
    $.ajax(v, {
        success: function (text) {
            console.log(v + ": " + text.length);
        }
    });
});

由于传统的for循环必然引入变量, 一般的函数式语言的语法中都不包含传统的for循环. == Scala == for循环是一个语法糖:

for (i <- a) { foo i }

相当于:

a each { i => foo i }

每次都会生成一个新的变量i, 以及包含这个i的新闭包 为了强调i不是变量, Scala标准废弃了下面这种语法:

for (var i <- a) { foo i }

Scheme

Scheme的do循环其实是一个宏:

(do ((i 0 (+ i 1)))
    ((= i 5) i)
  (foo i))

展开后就是一个递归调用:

(letrec
  ((loop (lambda (i)
    (if (= i 5)
      i
      (begin (foo i) (loop (+ i 1)))))))
  (loop 0))

对列表操作的时候最好使用srfi-1的map函数:

(map (lambda (i) (foo i)) a)

Haskell

Haskell提供的循环:

[ foo i | i <- a ]

或者使用list monad:

do
  i <- a
  return (foo i)

可以看出, 所有的函数式语言提供的for循环都没有使用变量, 变量在函数式语言中天生水土不服. javascript本身就是个畸形, 标榜自己是函数式语言, 却处处学习C/java的语法, 在使用javascript的时候, 最好尽量避免使用传统的for循环