JavaScript与Lisp,通向编程圣殿[1.2]: 《"树"的基础计算》深度解释

1936年,阿隆佐·邱奇提出 λ 演算,以函数为参数和返回值的函数,定义虚拟的机器编程系统。函数用希腊字母 λ 标识,这个形式系统因此得名。

<br />

1958年,MIT 的教授 John McCarthy 公开了表处理语言 Lisp。Lisp 是对阿隆佐·丘奇的 λ 演算系统的实现,同时工作在冯·诺依曼计算机上。

<br />

1973 年,MIT 人工智能实验室的一组程序员开发了被称为 Lisp 机器的硬件-阿隆佐 λ 演算的硬件实现!

JavaScript 对树的表示和计算,已经展现了其纯正的 Lisp 血统,也许不是那么直接。在这篇文字里,我想深度解释一下,“树”和“树”计算的思想是如何一层层的形成的。

“树”的表示法则

如果你看过JavaScript与Lisp,通向编程圣殿[1]: “树”的基础计算,应该明白 JavaScript 如何像 Lisp 一样来表示一棵树和计算树。

让我们重新简单清晰的分析一遍。

  A
 / \   
B   C

这是简单到不能再简单的树了,在JavaScript中你可以这样表示这棵树

['A', 'B', 'C']

在 Lisp 中,这样表示这棵树

(A B C)

规则非常简单:

树由节点组成,A B C 三者都是节点。树本身就是一个最大的节点,一个大的节点内嵌了 A B C这些更小的节点。这就是树的根本规律,通过这个规律你可以层层嵌套,从而形成递归。

<br />

使用数组表示一棵树的节点,并且层层嵌套这些数组,你就能轻而易举的像 Lisp 一样表示一棵树

  1. 嵌套

    既然节点是嵌套的,那么规律是非常简单的,我们很容易可以写出下面的树:

    •  ['A'] 
      

      这段数组表示什么样的树?

         A 
      
    •  ['A', 'B'] 
      

      这段数组表示什么样的树?

         A
        /
       B  
      
    •  ['A', 'B', ['C', 'D']] 
      

      这段数组表示什么样的树?

         A
        / \   
       B   C
          /
         D    
      
    •  ['A', ['B', 'E', ['F', 'G']], ['C', 'D']]
      

      这段数组表示什么样的树?

             A
          /     \   
         B       C
        / \     /
       E   F   D
          /
         G      
      

    在上面的树,我们可以随意的拿出其中的一个节点[‘F’, ‘G’],他是其中的一项字节点,也是一棵树。前面我们说过,树本身就是节点,所以,节点也是树是成立的:

         F
        /
       G
    

    用通俗的语言,我们可以这样认为,树是这样的结构:

    [节点, 节点, 节点, ...]
    

    而其中任何一个节点都可以是:

    [节点, 节点, ...]
    

    用图形表示一下,就是这样的:

    [节点, 节点, 节点, ...]
            |      \    \
            |       \    \
            v        \    \
    [节点, 节点, ...]  \    \
                      v     \   
          [节点, 节点, ...]   \
                             v
                            ...  
    
  2. 提取

    当我们想提取[图 6]中的一个节点时,怎么操作?如果这个树名字叫做tree,我们可以这样操作。

    • 选取第0个节点:tree[0]
    • 选取第1个节点:tree[1]
    • 选取第1个节点的第1个子节点:tree[1][1]
    • 选取第1个节点的第1个子节点的第1个子节点:tree[1][1][1]

树与递归

上边的已经显示了树的提取规律:

选取一个点A,然后选取A的子点B,再然后选取B的子点E,… 以此类推

这就是递归的本质:使用同一种操作方法,层层进入,操作遇到的每一个数据项。

  1. 写在递归之前

    也许你曾经见过 map 这样的类循环操作:

    [1, 2, 3].map(function (x) { return x*x; })
    

    这个 map 对数组[1, 2, 3]中每一个项进行平方。

    这就是一个最原始的递归,对数据项中的每一个运用同一个操作:x*x。只不过 map 还是非常简易和傻瓜化的,只能操作单层的数组,一旦遇到

    [1, [2, 6], 3]
    

    这样的数组时,他就无能为力了。map 的源代码可以这样表示:

    function map(f) {
        var length = this.length;
        return (function walk(x, i, array, result) {
            if (i === length) {
                return result;    
            }
            // 对每一个数据项求值
            var value = f(x);
            // 递归下一个数据项
            return walk(array[i + 1], i + 1, array, result.concat(value));
        }(this[0], 0, this, [])); 
    }
    
    // x      : Int,   数据项
    // i      : Int,   数据项位置
    // array  : Array, 数据列表,数据集
    // result : Array, 结果,新的数据列表,新的结果集
    
  2. 开始递归

    上面我们说了,map 遇到多层的数组时就爱莫能助了。JavaScript 如同 Lisp 一样,当你需要一个更复杂的工具时,你完全可以手工自己构建。

    一个复杂的工具,我们起个名字叫做 mmap,如何操作

    [1, [2, 6], 3]
    

    这个多层数组呢?

    比如,我们在控制台上输入

    mmap([1, [2, 6], 3], function (x) { return x*x; })
    

    我们希望得到一个求平方后的结果:

    [1, [4, 36], 9] 
    

    让我们来实现他。

    计算的方式,有必要说明一下:

    任何复杂的循环,迭代,递归,都可以简化为对 2 个项的求值,即:对第一项求值和对后面所有项求值。斐波那契数列?没错,我觉得,斐波那契数列完全解释了递归的本质。

    <br />

    第一项我可以求值,那么后面的项怎么求值?很简单,同对第一项求值同样的道理:把后面的所有项作为一个新的数组,取出第一项求值,并把剩余的作为一个新的后面所有项,然后重复重复再重复。

    根据这个规律,我们来推导过程:

    第一项(正在计算的)   后面的所有项       新的结果集       求平方
    
                      [1, [2, 6], 3] 
    1                 [[2, 6], 3]       计算 1          1  
    [2, 6]            [3]               计算 [2, 6]  
    2                 [6]                    计算 2     4 
    6                 []                     计算 60    36
    []                undefined              计算 []    
    3                 []                计算 3          9
    []                undefined              计算 []              
    

    程序如下:

    function mmap(list, f) {
        if (list instanceof Array && list.length === 0) { // 对[]求值
            return [];
        }   
        if (typeof list === 'number') { // 对数据项求值
            return f(list);          
        }
        var doing   = list.shift();      // 第一项
        var watings = list;              // 后面的所有项
        var first   = mmap(doing, f);    // 对第一项求值
        var lasts   = mmap(watings, f);  // 对后面所有项求值 
        // 合并第一项的值和后面项的值,形成一个新的结果集
        return [first].concat(lasts);  
    }
    
    console.log(mmap([1, [2, 6], 3], function (x) { return x*x; }));
    
    // => [ 1, [ 4, 36 ], 9 ]
    

    让我们再修改一下,让程序更 Lisp :

    function mmap(list, f) {
        return list instanceof Array && list.length === 0
               ? []
               : typeof list === 'number'
                 ? f(list)
                 : [mmap(list.shift(), f)].concat(mmap(list, f));
    }
    
    console.log(mmap([1, [2, 6], 3], function (x) { return x*x; }));
    
    // => [ 1, [ 4, 36 ], 9 ]
    

    你也可以计算超级复杂的数组,比如

    var list = [1, [2, [6, 9], [3, [2, [7, 6]]]], 3];
    
    console.log('%j', mmap(list, function (x) { return x*x; }));
    
    // => [1, [4, [36, 81], [9, [4, [49, 36]]]], 9]
    

最后,再来聊聊 Lisp

[1, [6, [6, 9], [3, [2, [7, 6]]]], 3]

表示了一棵深度颇高的树,用在 Lisp 会这么表示:

(1 (6 (6 9) (3 (2 (7 6)))) 3)

足够说明 JavaScript 具有非常纯正的 Lisp 血统。

最后,放上我们的 Lisp mmap:

(define mmap
 (lambda (lst f)
  (cond
   ((null? lst) '())
   ((list? lst) (cons (mmap (car lst) f) (mmap (cdr lst) f)))
   (else (f lst)))))

(display (mmap '(1 (6 (6 9) (3 (2 (7 6)))) 3) (lambda (x) (* x x))))

// => (1 (36 (36 81) (9 (4 (49 36)))) 9)

友情提示:为了展示方便直观,本文中的数组操作使用了shift(),这会造成外部数组的破坏。实际使用时可以灵活采取splice(),[i],concat(),shift(),pop()不必拘泥于形式。

    原文作者:Tulayang
    原文地址: https://www.jianshu.com/p/eea5c0a601ef
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞