对Lisp语法的直观认识

2020-10-26 11:23:41

我遇到的每个LISP黑客,包括我自己,都认为Lisp中的所有括号都令人讨厌和奇怪。当然,一开始是这样的。不久之后,我们都得到了同样的顿悟:LISP的力量在于这些括号!在这篇文章中,我们将踏上一段通往那个顿悟的旅程。

假设我们正在创建一个让你画东西的程序。如果我们用JavaScript编写,我们可能会有如下函数:

DraPoint({x:0,y:1},';黄色';)draLine({x:0,y:0},{x:1,y:1},';蓝色';)draCircle(point,Radius,';;red';)Rotate(Shape,90)...。

这意味着用户将能够向您的屏幕“发送”指令,您将看到他们的绘画栩栩如生。

好吧,假设我们设置了一个网络插座连接。我们可以收到来自用户的指令,如下所示:

要使其即时工作,一种选择是将代码字符串作为输入:

现在,用户可以发送";draLine({x:0,y:0},{x:1,y:1},';red';)";和bam:我们要画一条线!

但是…。你的蜘蛛感可能已经刺痛了。如果用户是恶意的,并且设法向我们发送了如下指令,该怎么办:

哦哦…。我们的cookie会被发送到iwill p3wn.com,而恶意用户确实会对我们进行攻击。我们不能用EVAL,太危险了。

这就是我们的问题所在:我们不能使用eval,但我们需要某种方式来接收任意指令。

我们可以将这些指令表示为JSON。我们可以将每个JSON指令映射到一个特殊的函数,这样我们就可以控制运行什么。以下是我们可以表示它的一种方式:

{说明:[{functionName:";drawLine";,args:[{x:0,y:0},{x:1,y:1},";Blue";]},];}。

让我们看看能不能把这里清理干净。下面是我们的JSON:

{说明:[{functionName:";drawLine";,args:[{x:0,y:0},{x:1,y:1},";Blue";]},];}。

因为每条指令都有functionName和args,所以我们不需要详细说明。我们可以这样写:

{说明:[[";DrawLine";,{x:0,y:0},{x:1,y:1},";Blue";]],}

好的!。我们将对象更改为支持数组。要处理这一点,我们只需要一条规则:指令的第一部分是函数名,其余部分是参数。如果我们把它写下来,我们的onMessage会是这样的:

WebSocket.onMessage(data=>;{const fns={drawLine:drawLine,...};data.。说明.forEach(([fnName,...。Args])=>;fns\[fnName\](...。Args));})。

DrawLine({x:0,y:0},{x:1,y:1},';Blue';)//与[";DrawLine";,{x:0,y:0},{x:1,y:1}]相同。

[";Rotate";,[";DrawLine";,{x:0,y:0},{x:1,y:1}],90]。

这里,Rotate指令有一个本身就是指令的参数!相当强大。令人惊讶的是,我们只需稍微调整代码即可使其正常工作:

WebSocket.onMessage(Data=>;{const fns={drawLine:drawLine,...};const parseInstruction=(Ins)=>;{if(!Array.isArray(Ins)){//这必须是基元参数,如{x:0 y:0}return ins;}const[fName,...。Args]=INS;fns\[fName\](...。Args);};数据。指令.forEach(ParseInstruction);})。

不错,我们引入了parseInstruction函数。我们可以递归地将parseInstruction应用于参数,并支持如下内容:

[";Rotate";,[";Rotate";,[";DrawLine";,{x:0,y:0},{x:1,y:1}],90]]

{说明:[[";DrawLine";,{x:0,y:0},{x:1,y:1}]],}。

我们可以有一个名为do的特殊指令,而不是顶级键,它运行它给出的所有指令。

WebSocket.onMessage(数据=>;{const fns={...。做:(……。Args)=>;args[args.length-1],};const parseInstruction=(INS)=>;{if(!Array.isArray(Ins)){//这必须是基元参数,如{x:0,y:0}return ins;}const[fName,...。Args]=ins;返回fns\[fName\](...。Args.map(ParseInstruction);};parseInstruction(指令);})。

哦,哇,那太简单了。我们刚刚在FNS中添加了DO。现在我们可以支持这样的指令:

[";DO";,[";DrawPoint";,{x:0,y:0}],[";Rotate";,[";DrawLine";,{x:0,y:0},{x:1,y:1}],90]],];

如果我们可以支持定义,我们的远程用户就可以编写一些非常有表现力的指令!让我们将代码转换为我们一直使用的数据结构:

[";def";,";Shape";,[";DrawLine";,{x:0,y:0},{x:1,y:1}]][";Rotate";,";Shape";,90]。

没什么不好的!如果我们能支持这样的指令,我们就大功告成了!以下是方法:

WebSocket.onMessage(数据=>;{常量变量={};常量fns={...。Def:(name,v)=>;{defs[name]=v;},};const parseInstruction=(Ins)=>;{if(Variables[Ins]){//这必须是某种变量,如";Shape";Return Variables[Ins];}If(!Array.isArray(Ins)){//这必须是基元参数,如{x:0 y:0}return ins;}const[fName,...。Args]=ins;返回fns\[fName\](...。Args.map(ParseInstruction);};parseInstruction(指令);})。

在这里,我们引入了一个Variables对象,它跟踪我们定义的每个变量。一个特殊的def函数会更新该Variables对象。现在我们可以运行以下指令:

[";do";,[";def";,";Shape";,[";DrawLine";,{x:0,y:0},{x:1,y:1}]],[";旋转";,";Shape";,90],];

让我们更上一层楼吧。如果我们让我们的远程用户定义他们自己的功能会怎么样?

Const draTriangle=function(Left,top,right,color){drawLine(Left,top,color);draLine(top,right,color);DrawLine(Left,Right,color);}DrawTriangle(...)。

我们该怎么做呢?让我们再跟着我们的直觉走一遍。如果我们将其转录为我们的数据表示形式,则如下所示:

[";def";,";Drag三角形";,[";FN";,[";Left";,";,";Right";,";color";],[";Do";,[";DrawLine";,";Left";,";top";,";color";],[";drawLine";,";top";,";right";,";color";],[";drawLine";,";Left";,";right";,";color";],[";draTriangle";,{x:0,y:0},{x:3,y:3},{x:6,y:0},";蓝色";],

我们所需要做的就是以某种方式解析这条指令,然后砰的一声,我们就可以开始了!

完成这项工作的关键是我们的[#34;fn";,…]。指令。如果我们这样做会怎么样:

Const parseFnInstruction=(args,body,oldVariables)=>;{return(...。值)=>;{const newVariables={...。旧的变量,..。MapArgsWithValues(args,value),};return parseInstructions(Body,newVariables);};};

当我们找到Fn指令时,我们运行parseFnInstruction。这将产生一个新的javascript函数。我们将使用该函数替换此处的DrawTriangle:

[";draTriangle";,{x:0,y:0},{x:3,y:3},{x:6,y:0},";蓝色";]。

[{x:0,y:0},{x:3,y:3},{x:6,y:0},";蓝色";]。

将创建一个新的Variables对象,该对象包括函数参数到这些新提供的值的映射:

Const newVariables={...。旧变量,左侧:{x:0,y:0},顶部:{x:3,y:3},右侧:{x:6,y:0},颜色:";蓝色";,}。

[";Do";,[";drawLine";,";Left";,";,";color";],[";DrawLine";,";top";,";,";color";],[";DrawLine";];,";左";,";右";,";颜色";],],

并使用我们的新变量通过parseInstructions运行它。这样,会将";LEFT";作为变量进行查找并映射到{x:0,y:0}。

让我们继续执行我们的计划吧。我们需要做的第一件事是让parseInstructions接受变量作为参数:

Const parseInstruction=(ins,变量)=>;{...。返回fn(...。Args.map((Arg)=>;parseInstruction(arg,变量);};parseInstruction(指令,变量);

接下来,我们将添加一个特殊检查,以检测是否有“fn”指令:

Const parseInstruction=(ins,变量)=>;{...。Const[fName,...。Args]=ins;if(fName==";fn";){return parseFnInstruction...。参数,变量);}...。返回fn(...。Args.map((Arg)=>;parseInstruction(arg,变量);};parseInstruction(指令,变量);

Const mapArgsWithValues=(args,Values)=>;{return args.duce((res,k,idx)=>;{res[k]=values[idx];return res;},{});}const parseFnInstructions=(args,body,oldVariables)=>;{return(...。值)=>;{const newVariables={...。旧的变量,..。MapArgsWithValues(args,value)}return parseInstructions(Body,newVariables);};};

它完全按照我们说的那样工作。我们返回一个新函数。当它运行时,它:

Const parseInstruction=(ins,变量)=>;{...。Const[fName,...。Args]=ins;if(fName==";fn";){return makeFn(...。Args,Variables);}const fn=fns[fName]||变量[fName];返回fn(...。Args.map((Arg)=>;parseInstruction(arg,Variables);

这里,由于fn现在既可以来自fn,也可以来自变量,所以我们同时检查两者。把所有这些放在一起,它就会起作用!

WebSocket.onMessage(Data=>;{const Variables={};const fns={drawLine:DrawLine,DrawPoint:DrawPoint,Rotate:Rotate,Do:(...。Args)=>;args[args.length-1],def:(name,v)=>;{变量[name]=v;},};const mapArgsWithValues=(args,value)=>;{return args.duce((res,k,idx)=>;{res[k]=values[idx];return res;},{});};Const parseFnInstruction=(args,body,oldVariables)=>;{return(...。值)=>;{const newVariables={...。旧的变量,..。MapArgsWithValues(args,Values),};return parseInstructions(body,newVariables);};};const parseInstruction=(ins,Variables)=>;{if(Variables[Ins]){//这必须是某种变量返回变量[Ins];}if(!Array.isArray(Ins)){//这必须是基元参数,如{x:0 y:0}return ins;}const[fName,...。Args]=ins;if(fName==";fn";){return parseFnInstruction...。Args,Variables);}const fn=fns[fName]||变量[fName];返回fn(...。Args.map((Arg)=>;parseInstruction(arg,变量);};parseInstruction(指令,变量);})。

[";Do";,[";def";,";draTriangle";,[";FN";,[";Left";,";top";,";Right";,";color";],[";Do";,[";DrawLine";,";左侧";,";顶部";,";color";],[";DrawLine";,";顶部";,";右侧";,";颜色";],[";drawLine";,";左侧";,";右侧";,";color";],[";drawTriangle";,{x:0,y:0},{x:3,y:3},{x:6,y:0},";蓝色";],[";drawTriangle";,{x:6,y:6},{x:10,y:10},{x:6,y:16},";紫色";],])。

我们可以编写函数,我们可以定义变量,甚至可以创建我们自己的函数。如果我们仔细想想,我们刚刚创建了一种编程语言!

我们甚至可能会注意到一些有趣的事情。我们新的数组语言比JavaScript本身更有优势!

在JavaScript中,您可以通过编写const x=foo来定义变量。假设您希望将const“重写”为c。您不能这样做,因为const x=foo是JavaScript中的特殊语法。你不能改变这一点。

但是,在我们的数组语言中,根本没有语法!所有东西都只是数组。我们可以很容易地编写一些特殊的c指令,其工作方式与def类似。

如果我们仔细想想,就好像在Javascript中我们是客人,我们需要遵循语言设计者的规则。但在我们的数组语言中,我们是“共同所有者”。语言设计者编写的“内置”内容(“def”、“fn”)和我们编写的内容没有太大区别!(“Drawing Triangle”)。

还有另一个更响亮的胜利。如果我们的代码只是一堆数组,我们可以对代码做一些操作。我们可以编写生成代码的代码!

这是很难做到的。我们需要像babel这样的东西来解析我们的文件,并在AST之上工作,以确保我们安全地重写代码。

但是在我们的数组语言中,我们的代码只是数组!它很容易重写,除非:

将代码表示为数据不仅仅允许您轻松地操作代码。它还允许您的编辑器也这样做。例如,假设您正在编辑此代码:

如果您是了解这些数组的编辑,您可以告诉它:向右“展开”此区域:

突然之间,您编辑的不是字符,而是代码的结构。这就是所谓的结构化编辑2。它可以帮助你以雕刻家的速度移动,这也是当你的代码是数据时你将获得的众多胜利之一。

嗯,您碰巧发现的这种数组语言是…。是Lisp的一种实现不佳的方言!

[";Do";,[";def";,";draTriangle";,[";FN";,[";Left";,";top";,";Right";,";color";],[";Do";,[";DrawLine";,";左侧";,";顶部";,";color";],[";DrawLine";,";顶部";,";右侧";,";颜色";],[";drawLine";,";左侧";,";右侧";,";color";],[";drawTriangle";,{x:0,y:0},{x:3,y:3},{x:6,y:0},";蓝色";],[";drawTriangle";,{x:6,y:6},{x:10,y:10},{x:6,y:16},";紫色";],])

(DO(定义绘制三角形(FN[左上右上颜色](画线左上颜色)(画线右上颜色)(绘制三角形{:x0:y0}{:x3:y3}{:x6:y0}";蓝色";)(绘制三角形{:x6:y6}{:x10:y10}{:x6:y16}";紫色";)。

现在,如果我们同意操纵源代码的能力对我们来说很重要,那么什么样的语言最有利于支持它呢?

我们可以解决这个问题的一种方法是重新表述:我们如何才能使操作代码与操作代码中的数据一样直观?答案冒出来了:制作代码数据!这是一个多么激动人心的结论。如果我们关心操纵源代码,我们就会得到答案:代码必须是数据3。

如果代码必须是数据,那么我们可以使用什么样的数据表示呢?XML可以工作,JSON可以工作,不胜枚举。但是,如果我们试图找到最简单的数据结构,会发生什么呢?如果我们继续简化,我们就会滑向所有…中最简单的嵌套结构。清单!

它很有启发性,从某种意义上说,Lisp似乎是被“发现”的。这就像是优化问题的解决方案:如果您关心代码操作,那么您会倾向于使用Lisp。使用已发现的工具有一些令人敬畏的东西:谁知道,外星生命形式可能会使用Lisp!

这很令人兴奋,因为可能有更好的语法。我们不知道。在我看来,Ruby和Python在哪里做实验,试图带来LISP式的功能,没有括号。我认为这个问题还没有解决。也许你可以考虑一下,🙂。

您可以想象,如果您可以重写您的语言所用的代码,那么您的表现力会有多强。您将真正处于与语言设计人员相同的地位,并且您可以在该级别编写的抽象加起来可以节省您多年的工作。

感谢Daniel Woelfel,Alex Kotliarskyi,Sean Grove,Joe Averbukh,Irakli Safareli审阅这篇文章的草稿