极致之美——百行代码实现全新智能语言第1/6页

首先要解释一下:
“极致之美”不是说月儿的这篇文章,因为本人还没有自大到这种程度:P,它形容的是Lisp和javascript结合的优美形态。
本来以下内容是要在无优首发的,但是不巧完成文章的当天忽然发现无优“弹”了,直到上周末才恢复=.=,由于不能等那么久,所以就先放到月儿在CSDN上的博客里去了。
正如标题所描述的,下文是关于用javascript实现类Lisp语言的技巧,然而重点不在于如何实现一门编程语言,而是在于通过思考和实现过程展示javascript的简洁灵活和Lisp的优美。
或许这里接触Lisp的人不多,因此不少人一定会对以下的内容或形式感到奇怪,如果你完全没有接触过它,不必过分惊讶,Lisp的确与以前你见过得所有编程语言不同,因为,呃,它是Lisp,独一无二的Lisp,一段优雅、简洁、完整、独立的奇妙思想,也许你会觉得它很难懂,但是一旦你懂了,你会喜欢上它的。
好了,下面开始我们的LispScript之旅~
最近在网上偶然看到一篇文章,说javascript = C+Lisp,于是思考这样的问题,既然javascript包含着部分Lisp的血统,那么用javascript来实现一个类似于Lisp的人工智能脚本又会是什么样子?
LISt Processing语系作为一种“函数式”语系,自从诞生之日起便以其简单优美的风格和简洁高效的结构征服了许许多多的研究者和爱好者。
目前这种古老的语言和文法仍然被许许多多的人使用着并热爱着,而且在人工智能等领域发挥着非常巨大的作用。
我认为,javascript的灵活加上Lisp的简洁,应该能够创造出一种非常优美的语言,不过这种语言是什么样子的呢?相信大家也很想知道,那么下面我们一起来研究一下这个非常吸引人的问题。
(在仔细阅读下面的内容之前,建议大家先倒杯热茶,坐下来平静一下自己的心情,深呼吸一下,集中起精神来,因为下面的过程将是有趣而又颇耗脑细胞的...^^)
在进入Lisp王国之前,让我们先来做一些javascrip的准备工作...请仔细阅读下面的代码
NIL = [];
Array.prototype.toEvalString = function()
{
 if(this.length <= 0) return "NIL";
 var str = "";
 for (var i = 0; i < this.length; i++)
 {
  if(this[i] instanceof Array)
   str += "," + this[i].toEvalString();
  else str += "," + this[i];
 }
 return "[" + str.slice(1) + "]";
};
(function(){
 LispScript = {
  Run : run
 };
 function run(code)
 {
  if(code instanceof Array)
  {
   var elements = new Array();
   for (var i = 0; i < code.length; i++)
   {
    code[i] = run(code[i]); //递归向下读取
    if(code[i] instanceof Function)  //解析表达式
    {
     if(code[i].length <= 0) //无参函数可省略[]直接以函数名称调用
     {
      code[i] = code[i].call(null);
     }
     else if(i == 0)  //调用带参数的函数[funcall,args...]
     {
      return code[i].apply(null, code.slice(1));
     }
    }
   }
   return code;
  }
  return Element(code);
 };
})();
function Assert(msg, cond)
{
 if(cond)
  return true;
 else
  {
   alert(msg);
   throw new Error(msg);
  }
};
function Element(arg)
{
 if(arg == null)
  return [];
 else if(arg instanceof Function && arg.length <= 0)
  return arg.call(null);
 else
  return arg;
};
__funList = new Array();

以上这段简简单单不过数十行的javascript代码由三个辅助函数、一个主体对象、一个常量NIL(后面我们会知道它表示一个空表或者逻辑false),以及一个存放函数名称的堆栈组成。
LispScript静态对象构成了LispScript解析器的主体,它只有一个Run方法,该方法用向下递归的方式解析传递进来的LispScript代码,代码的类型——相信细心的读者已经发现了——直接用的是javascript的数组,也就是一系列“[”、“]”和分隔符“,”构成的序列。
用javascript天然的数组特性,使得我们的解析器可以设计得十分简洁——不用去拆分和解析每一个token,于是一段简短到不到50行的代码惊人地实现了整个LispScript解析器的核心!
三个辅助函数的作用分别是为函数迭代提供解析(toEvalString),检测序列异常(Assert,后面的具体实现中其实并没有用到),以及解析指令单词(Element)
接下来我们先定义表达式.表达式或是一个原子[atom],它是一个字母序列(如 foo),或是一个由零个或多个表达式组成的表(list), 表达式之间用逗号分开, 放入一对中括号中. 以下是一些表达式: 
(注:原Lisp语法的表达式用空格隔开,放入一对括号中。因是javascript的实现,所以用中括号和逗号较为简洁)
foo
[]
[foo]
[foo,bar]
[a,b,[c],d]
最后一个表达式是由四个元素组成的表, 第三个元素本身是由一个元素组成的表. 
在算术中表达式 1 + 1 得出值2. 正确的Lisp表达式也有值. 如果表达式e得出值v,我们说e返回v. 下一步我们将定义几种表达式以及它们的返回值. 
如果一个表达式是表,我们称第一个元素为操作符,其余的元素为自变量.我们将定义七个原始(从公理的意义上说)操作符: quote,atom,eq,car,cdr,cons,和 cond. 
[quote,x] 返回x. 我们把[quote,x]简记为[_,x]. 
> [quote,a]
a
> [_,a]
a
> [quote,[a b c]]
[a,b,c]
quote = _ = function(args)
{
 if(arguments.length < 1)
  return [];
 else if(arguments.length >= 1)
 {
  return arguments[0];
 }
};

[atom,x]返回原子true如果x的值是一个原子或是空表,否则返回[]. 在Lisp中我们按惯例用原子true表示真, 而用空表表示假. 
> [atom,[_,a]]
true
> [atom,[_,[a,b,c]]]
[]
> [atom,[_,[]]]
true
atom = function(arg)
{
 var tmp = LispScript.Run(arg); //先对参数求值
 if(!(tmp instanceof Array) || tmp.length <= 0)
  return true;
 else
  return [];
};

既然有了一个自变量需要求值的操作符, 我们可以看一下quote的作用. 通过引用(quote)一个表,我们避免它被求值. 一个未被引用的表作为自变量传给象 atom这样的操作符将被视为代码: 
> [atom,[atom,[_,a]]]
true
反之一个被引用的表仅被视为表, 在此例中就是有两个元素的表: 
> [atom,[_,[atom,[_,a]]]]
[]
这与我们在英语中使用引号的方式一致. Cambridge(剑桥)是一个位于麻萨诸塞州有90000人口的城镇. 而"Cambridge"是一个由9个字母组成的单词. 
引用看上去可能有点奇怪因为极少有其它语言有类似的概念. 它和Lisp最与众不同的特征紧密联系:代码和数据由相同的数据结构构成, 而我们用quote操作符来区分它们. 
[eq,x,y]返回t如果x和y的值是同一个原子或都是空表, 否则返回[]. 
> [eq,[_,a],[_,a]]
true
> [eq,[_,a],[_,b]]
[]
> [eq,[_,[]],[_,[]]]
true
equal = eq = function(arg1, arg2)
{
 var tmp1 = LispScript.Run(arg1);
 var tmp2 = LispScript.Run(arg2);   //先对参数求值
 if(!(tmp1 instanceof Array) && !(tmp2 instanceof Array) && 
  tmp1.toString() == tmp2.toString() || 
  (tmp1 instanceof Function) && (tmp2 instanceof Function) && tmp1.toString() == tmp2.toString() ||
  (tmp1 instanceof Array) && (tmp2 instanceof Array) && (tmp1.length == 0) && (tmp2.length == 0))
  return true;
 else
  return [];
};

[car,x]期望x的值是一个表并且返回x的第一个元素. 
> [car,[_,[a b c]]]
a
car = function(arg)
{
 var tmp = LispScript.Run(arg);  //先对参数求值
 if(tmp instanceof Array && tmp.length > 0)
  return tmp[0];
 else
  return [];
};

[cdr,x]期望x的值是一个表并且返回x的第一个元素之后的所有元素. 
> [cdr,[_,[a b c]]]
[b,c]
cdr = function(arg)
{
 var tmp = LispScript.Run(arg);  //先对参数求值
 if(tmp instanceof Array && tmp.length > 0)
  return tmp.slice(1);
 else
  return []; 
};

[cons,x,y]期望y的值是一个表并且返回一个新表,它的第一个元素是x的值, 后面跟着y的值的各个元素. 
> [cons,[_,a],[_,[b,c]]]
[a,b,c]
> [cons,[_,a],[cons,[_,b],[cons,[_,c],[_,[]]]]]
[a,b,c]
> [car,[cons,[_,a],[_,[b c]]]]
a
> [cdr,[cons,[_,a],[_,[b,c]]]]
[b,c]
cons = function(arg1, arg2)
{
 var tmp1 = LispScript.Run(arg1);
 var tmp2 = LispScript.Run(arg2);   //先对参数求值
 if(tmp2 instanceof Array)
 {
  var list = new Array();
  list.push(tmp1);
  return list.concat(tmp2);
 }
 else
  return [];
};

[cond [...] ...[...]] 的求值规则如下. p表达式依次求值直到有一个返回t. 如果能找到这样的p表达式,相应的e表达式的值作为整个cond表达式的返回值. 
> [cond,[[eq,[_,a],[_,b]],[_,first]],
      [,[atom,[_,a]], [_,second]]]
second
cond = function(args)
{
 for (var i = 0; i < arguments.length; i++)
 {
  if(arguments[i] instanceof Array)
  {
   var cond = LispScript.Run(arguments[i][0]);  //先对参数求值
   //alert(cond);
   if(cond == true && arguments[i][1] != null)
    return LispScript.Run(arguments[i][1]);
  }
 }
 return [];
};

当表达式以七个原始操作符中的五个开头时,它的自变量总是要求值的.2 我们称这样 的操作符为函数. 
接着我们定义一个记号来描述函数.函数表示为[lambda, [...], e],其中 ...是原子(叫做参数),e是表达式. 如果表达式的第一个元素形式如上 
[[lambda,[...],e],...]
则称为函数调用.它的值计算如下.每一个表达式先求值,然后e再求值.在e的求值过程中,每个出现在e中的的值是相应的在最近一次的函数调用中的值. 
> [[lambda,['x'],[cons,'x',[_,[c]]]],[_,a]]
[a,c]
> [[lambda,['x','y'],[cons,'x',[cdr,'y']]],[_,z],[_,[a,b,c]]]
[z,b,c]
lambda = function(args, code)
{
 if(code instanceof Array)
 {
  var fun = new Function(args, 
   "for(var i = 0; i < arguments.length; i++) arguments[i] = LispScript.Run(arguments[i]);return LispScript.Run("+code.toEvalString()+");");
  var globalFuncName = __funList.pop();
  fun._funName = globalFuncName;
  if(globalFuncName != null)
   self[globalFuncName] = fun;
  return fun;
 }
 return [];
};

如果一个表达式的第一个元素f是原子且f不是原始操作符 
[f ...] 
并且f的值是一个函数[lambda,[...]],则以上表达式的值就是 
[[lambda,[...],e],...]
的值. 换句话说,参数在表达式中不但可以作为自变量也可以作为操作符使用: 
> [[lambda,[f],[f,[_,[b,c]]],[_,[lambda,[x],[cons,[_,a],x]]]
[a,b,c]
有另外一个函数记号使得函数能提及它本身,这样我们就能方便地定义递归函数.记号 
[label,f,[lambda,[...],e]] 
表示一个象[lambda,[...],e]那样的函数,加上这样的特性: 任何出现在e中的f将求值为此label表达式, 就好象f是此函数的参数. 
假设我们要定义函数[subst,x,y,z], 它取表达式x,原子y和表z做参数,返回一个象z那样的表, 不过z中出现的y(在任何嵌套层次上)被x代替. 
> [subst,[_,m],[_,b],[_,[a,b,[a,b,c],d]]]
[a,m,[a,m,c],d]

当前1/6页 123456下一页阅读全文

(0)

相关推荐

  • 极致之美——百行代码实现全新智能语言第1/6页

    首先要解释一下: "极致之美"不是说月儿的这篇文章,因为本人还没有自大到这种程度:P,它形容的是Lisp和javascript结合的优美形态. 本来以下内容是要在无优首发的,但是不巧完成文章的当天忽然发现无优"弹"了,直到上周末才恢复=.=,由于不能等那么久,所以就先放到月儿在CSDN上的博客里去了. 正如标题所描述的,下文是关于用javascript实现类Lisp语言的技巧,然而重点不在于如何实现一门编程语言,而是在于通过思考和实现过程展示javascript的简

  • python百行代码自制电脑端网速悬浮窗的实现

    前言 看到某60的网速悬浮球有点心动,但是又不想装这个流氓软件,就自己用python加PyQt5自制了一个,实测还行,关键不占用电脑一点资源,已将软件打包,可自行下载使用. 预览 观看直播时实时网速. 文件结构 运行管理 开始运行时内存消耗18.3m,cpu,磁盘,网络不占用. 运行一天后内存稳定于6.4m,cpu,磁盘,网络不占用. 整体思路 使用psuti.net_io_counters 监控电脑网卡IO 将流量数据格式化,统计每次数据总和保存在本地<流量使用情况.txt>(这个是个缺陷,

  • C/C++百行代码实现热门游戏消消乐功能的示例代码

    游戏设计 首先我们需要使用第三方框架,这里我使用的是sfml,不会使用sfml在我的上几篇文章当中-扫雷(上)有详细的开发环境搭建介绍 首先准备图片资源 一张背景图片,一张宝石图片 窗口初始化加载图片 Texture t1; t1.loadFromFile("images/bg2.png"); 当鼠标第一次单击时,记录下位置,第二次单击又记录一下位置,如果两个小方块相邻就交换位置,如果不相邻如图c的位置则,不发生变化 判断行或列如果三张一样的图片相邻,清除一下图片,进行刷新 实列 #i

  • python百行代码实现汉服圈图片爬取

    目录 分析网站 子链接获取 获取标题和图片地址 保存图片 主函数 平时旅游的时候,在旅游景区我们经常可以看到穿各种服饰去拍照的游客,也不会刻意多关注.前两天浏览网页无意看到一个网站,看到穿汉服的女孩是真的很好看.无论是工作需要还是创作文案,把这么漂亮的图片来当作素材都是一个很好的idea.有需要,我们就爬它,爬它,爬它! 话不多说,我们下面详细介绍图片爬取. 分析网站 网址如下: https://www.aihanfu.com/zixun/tushang-1/ 这是第一页的网址,根据观察,第二页

  • C语言百行代码绘制圣诞水晶球

    目录 序 项目代码 总结 序 我爱你,不是因为你是一个怎样的人,而是因为我喜欢与你在一起时的感觉. 嗨!这里是狐狸~~ 今天就是圣诞节了,再过一个星期就是2022年了,最近总是感觉伤感,有些事情就是比想象中来的快一些,希望大家都可以把握2021年最后的时间,不留遗憾吧,后天圣诞节,今天再教大家一个圣诞项目吧,圣诞水晶球,今天这个呢代码不多,但难度会有点,因为这个涉及桌面,就是可以在桌面实现,希望大家可以认真看,认真学吧. 同样,先给大家看效果吧 效果还是很不错的,再加上一个音乐,女朋友看完就马上

  • Python三百行代码实现飞机大战

    目录 一. 动态效果图如下 二. 思路框架 三. Python代码实现 四. 小结 一. 动态效果图如下 先来看下飞机大战游戏最终实现的动态效果图. 二. 思路框架 plane_sprite.py文件内容 1.导入需要使用的模块 import random import pygame 在导入pygame之前,需要先使用命令: pip install pygame 进行包模块的安装 2.设置屏幕大小和刷新帧率等常量 3.创建继承于pygame.sprite.Sprite的基类GameSprite

  • 百行代码实现基于Redis的可靠延迟队列

    目录 原理详解 pending2ReadyScript ready2UnackScript unack2RetryScript ack consume 在之前探讨延时队列的文章中我们提到了 redisson delayqueue 使用 redis 的有序集合结构实现延时队列,遗憾的是 go 语言社区中并无类似的库.不过问题不大,没有轮子我们自己造

  • php 3行代码的分页算法(求起始页和结束页)

    一个好的分页算法, 应该具有下面的优点: 当前页码应该尽量在正中间. 如果"首页"和"尾页"不可用(当前处于第一页或最后一页), 不要隐藏这两组文字, 以免链接按钮位置变动. 算法简单. 下面的算法具有前面1和3两个优点. 复制代码 代码如下: // $curr_index, 当前页码. // $link_count, 链接数量. // $page_count, 当前的数据的总页数. // $start, 显示时的起始页码. // $end, 显示时的终止页码. $

  • 神级程序员JavaScript300行代码搞定汉字转拼音

    一.汉字转拼音的现状 首先应该说,汉字转拼音是个强需求,比如联系人按拼音字母排序/筛选:比如目的地(典型如机票购买) 按拼音首字母分类等等.但是这个需求的解决方案,但好像没听过什么巧妙的实现(特别是浏览器端),大概都需要一个庞大的字典. 具体到JavaScript,查查github和npm,比较优秀的处理汉字转拼音的库有pinyin 和pinyinjs,可以看到,两者都自带了庞大的字典. 这些字典动辄几十上百KB(有的甚至几MB),想在浏览器端使用还是需要一些勇气的.所以当我们碰到汉字转拼音的需

  • 详解python百行有效代码实现汉诺塔小游戏(简约版)

    直接上代码: #左中右塔用一个列表存储 left = list() center = list() right = list() """ 初始化函数 """ def init(): size = input("(请友善输入整数,未写判断!)请输入层数:") #初始化塔列表,如5层 左边塔放 1-3-5-7-9,中间和右边放5个-1 for i in range(1,int(size) + 1): left.append(i*2

随机推荐