JavaScript与Lisp,通向编程圣殿[1]: "树"的基础计算

JavaScript在设计时,注入了Scheme的血液,虽然设计者为了“商业”目的,为其披上了“C外衣”和“面向对象的礼帽”,但是其本质上应该是Lisp的,Lisp的思想,我们可以在JavaScript作出相同的模拟,你只需要使用“子集”,抛开伪装的“C外衣”和华而不实的“面向对象礼帽”。

历史和趋势让我逐渐相信,在早期,我们需要更优化更省资源的编程方式,能让机器运转的代码更有价值,随着计算机硬件的进化,“节省资源”“高并发”“分布式”都将不再是需要考虑的,智能化的程序将会成为主体。

许多年前,许多人都嘲笑JavaScript,至今仍然有,只是因为其简易低效的浮点数计算和单纯难以预测的类型系统,至今还有人在拿类型系统说事,你甚至看到TypeScript加入了类型系统。

呵呵~~~

同学,Lisp中是没有类型系统声明的,即便假设编译器有,但是编写过程是没有的,这就是JavaScript为什么没有。

JavaScript大众化的“C外衣”让他在浏览器获得了至高无上的地位,但是只有明智之士才明白:JavaScript之所以可以流行,是凭借内部轻量、逻辑有序的数据结构和计算方式,而这些都是Lisp的灵魂所在。

再过几年,或者10年,高并发分布式将不再是主题,因为实现他们都非常简单。智能化的程序,可分析的程序将会成为主题。即数学将会成为真正的程序。现在已经有了一些JavaScript的机器学习库,甚至神经网络库,虽然还没有广泛使用,但是硬件速度的提升早晚会使其大放异彩。

所以,从现在开始,掌握JavaScript的Lisp本质是非常重要的,这能让我们写出更加“智慧”的程序。

树的基本

让我们来点实际的东西,无论言语多么鲜华,没有干脆清晰的解决问题,都是空洞的。所以,让我们从Lisp的本质开始,也是Lisp的唯一:列表开始。

在其他的编程语言学科,更喜欢用“Tree树”这个字来形容,那么让我们来看一棵树:

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

上边的这颗树,如何用程序来表示呢?
作为JavaScript,你可以用JSON来表示,用数组对象表示。

那么Lisp是怎么表示的?

(A (B F) (C G H) D E)

这就是Lisp的列表表示法,只有一个方式(元素 ...),然后以此递归。
我们可以写出相同的JavaScript方式:

['A', ['B', 'F'], ['C', 'G', 'H'], 'D', 'E']

仔细查看,你很容易就能得出一个规律:
他们都是使用数组嵌套的,并且递归

从某些层面来讲,JavaScript的作者完全为这门语言融入了Lisp的列表,虽然没有提供方便的car cdr函数,但是如果想要模拟,是很方便的。

更复杂的一棵树

让我们再把树做的稍复杂一点,看看应该怎么用JavaScript表示:

        A
   /  /   \  \
  B   C    D   E
 /   / \    \
F   G   H    I
   /     \
  J       K

根据我们上面的推导,可以这样表示这棵树:

['A', ['B', 'F'], ['C', ['G', 'J'], ['H', 'K']], ['D', 'I'], 'E']
  • 使用数组表示节点
  • 一棵树本身就是一个特别大的节点,所以树即是节点,树即是数组
  • 每一个主节点,是一个数组的第一项
  • 每一个数组第一项后面的所有元素,都是该数组第一项的直接字节点

如果使用Lisp来表示,那么这棵树就是一个列表:

(A (B F) (C (G J) (H K)) (D I) E)

现在,你应该了解,表示一棵树,在JavaScript中是多么容易,多么的Lisp吧!

为树添加计算

让我们写个程序,来把我们刚才的树绘制一下,来表示一下JavaScript的Lisp血统。

比如,我们写个程序,输入

['A', ['B', 'F'], ['C', ['G', 'J'], ['H', 'K']], ['D', 'I'], 'E']

在控制台上打印出这样的树图形:

A
  B
    F
  C
    G
      J
    H
      K
  D
    I
  E

每当节点的深度增加一个时,打印的字符前面加2个空格,这个树图形字符串表示如下:

A\n  B\n    F\n  C\n    G\n      J\n    H\n      K\n  D\n    I\n  E\n

现在让我们来编写Lisp血统的JavaScript程序:

  1. 一个计算空格的程序

    首先我们需要一个能计算空格的程序。当节点在0深度时,输出0个空格,第一个深度时,输出1个空格,第2个深度时,输出2个空格,这样我们才能有效打印

    A
      B
    ...
    

    这样的格式字符。

    function spaces(n) {
        return (function walk(i, spaces) {
            if (i <= 0) {
                return spaces;
            }
            return walk(i - 1, spaces + '  ');
        } (n, ''));
    }
    

    这是非常Lisp的程序,使用了递归的方式,来计算空格。
    当输入要求n的时候,这个程序递归计算,返回所有的空格。

    spaces(3) => '   ' 
    
  2. 一个计算字符的程序

    所有的递归操作都可以用“左->右”来表示
    我们不停的计算“左”的值,然后将他们相加

    定义计算(节点):
    如果节点是一个字符: 返回字符
    如果节点是一个空结构:返回''
    其他,有效节点:   返回结果 = 计算(节点第一项) + 计算(节点后边的项)

    function string(list, deep, isCarList) { 
        // 如果节点是一个字符:返回字符
        if (typeof list === 'string') {
            return spaces(deep) + list + '\n';
        }
        // 如果节点是一个空节点:返回''
        if (list instanceof Array && list.length === 0) {
            return '';
        }
        // 其他,有效节点:返回结果 = 计算(节点第一项) + 计算(节点后边的项)
        var car = list.shift(),
            cdr = list;  
        return isCarList
               ? string(car, deep, false) + string(cdr, deep + 1, false)
               : car instanceof Array
                 ? string(car, deep, true) + string(cdr, deep, false)
                 : string(car, deep, false) + string(cdr, deep, false);
    }   
    

    这个程序定义为string(list, deep, isCarList)。

    list就是每一个节点的值,最开始是
    ['A', ['B', 'F'], ['C', ['G', 'J'], ['H', 'K']], ['D', 'I'], 'E']。

    我们取list最左边的项,即list[0],为了与Lisp保持一致,我们采用了list.shift(),将第一项提取出来,剩下的list作为等待操作的新的list,然后对第一项和剩下的list进行递归求值。

    第一项有可能也是个数组。

    如果你仔细观察,你会发现这十分类似斐波那契数列,甚至就是斐波那契数列。

  3. 打印

    现在可以打印我们的程序了:

    console.log(string(['A', ['B', 'F'], ['C', ['G', 'J'], ['H', 'K']], ['D', 'I'], 'E'], 0, true));
    

    打开nodejs控制台,输入程序,你将能看到:

    A
      B
        F
      C
        G
          J
        H
          K
      D
        I
      E   
    

    这个树型图。

改良版本的树形打印程序

最后,发上一个完整的改良版本:

function spaces(n, star) {
    if (typeof star !== 'string') {
        star = ' ';
    }
    return (function walk(i, spaces) {
        if (i <= 0) {
            return spaces;
        }
        return walk(i - 1, spaces + star + star);
    } (n, ''));
}

function string(list, star) { 
    return (function walk(list, deep, isCarList) {
        if (typeof list === 'string') {
            return spaces(deep, star) + list + '\n' ;
        }
        if (list instanceof Array && list.length === 0) {
            return '';
        }
        var car = list.shift(), cdr = list;  
        return isCarList
               ? walk(car, deep, false) + walk(cdr, deep + 1, false)
               : car instanceof Array
                 ? walk(car, deep, true) + walk(cdr, deep, false)
                 : walk(car, deep, false) + walk(cdr, deep, false);    
    }(list, 0, true));
}

console.log(string(['A', ['B', 'F'], ['C', ['G', 'J'], ['H', 'K']], ['D', 'I'], 'E'], '--'));

这个程序将会输出这样的一个树:

A
----B
--------F
----C
--------G
------------J
--------H
------------K
----D
--------I
----E

备注:为了解释简单,此文中的代码并未做深入的计算优化,特别是缓存,如果你想了解如何通过缓存记忆进一步提升计算效率,可以参看算法技巧: 如何使用JavaScript编写高效的fabonacci数列

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容