用javascript實現人工智慧指令碼

來源:互聯網
上載者:User
javascript|指令碼  

最近在網上偶然看到一篇文章,說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',[_,[b]]]],[_,a]]
[a,b]
> [[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]

我們可以這樣表示此函數
[label,subst,[lambda,[x,y,z],
               [cond,[[atom,z],
                      [cond,[[eq,z,y],x],
                            true,z]]],
                     [true,[cons,[subst,x,y,[car,z]],
                               [subst,x,y,[cdr,z]]]]]]]

label = function(funName, funDef)
{
 __funList.push(funName);

 return LispScript.Run(funDef);
};

我們簡記f=[label,f,[lambda,[...],e]]為
[defun,f,[...],e]

defun = function(funName, args, code)
{
 __funList.push(funName);
 
 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 [];
};

於是

[defun,subst,[x,y,z],
  [cond,[[atom,z],
         [cond,[[eq,z,y],x],
               [true,z]]],
     [true,[cons,[subst,x,y,[car,z]],
                  [subst,x,y,[cdr,z]]]]]]

偶然地我們在這兒看到如何寫cond運算式的預設子句. 第一個元素是't的子句總是會成功的. 於是
[cond,[x,y],[[_,true],z]]

等同於我們在某些語言中寫的
if x then y else z

對於函數調用,具有如下結構:[FunName,[_,args]]
其中FunName是函數名稱,[_,args]是指定參數引用列表args
注意[FunName,args]也是合法的,但是和[FunName,[_,args]]有所區別,對於前者,指令在被調用之前先計算args的值,把計算出的值作為參數列表代入Function Compute(期望args計算結果為List),而後者的args參數列表在函數指令調用時才被計算

到這裡為止我們很高興地看到LispScript已經可以不依賴於javascript來擴充了

現在我們可以直接用LispScript定義一些新函數了:

函數:[isNull,x]測試它的自變數是否是空表.
LispScript.Run(
 [defun,'isNull',['x'],
  [eq,'x',[_,NIL]]]
);

> [isNull,[_,a]]
[]
> [isNull. [_,[]]]
//t

函數:[and,x,y]返回t如果它的兩個自變數都是t, 否則返回[].
LispScript.Run(
 [defun,'and',['x','y'],
  [cond,['x',[cond,['y',true],[true,NIL]],
   [true,NIL]]]]
);

> [and,[atom,[_,a]],[eq,[_,a],[_,a]]]
t
> [and,[atom,[_,a]],[eq,[_,a],[_,b]]]
[]

函數:[not,x]返回t如果它的自變數返回[],返回[]如果它的自變數返回t.
LispScript.Run(
 [defun,'not',['x'],
  [cond,['x',NIL],
        [true,true]]]
);

> [not,[eq,[_,a],[_,a]]]
[]
> [not,[eq,[_,a],[_,b]]]
t

函數:[append,x,y]取兩個表並返回它們的連結.
LispScript.Run(
 [defun,'append',['x','y'],
  [cond,[[isNull,'x'],'y'],
    [true,[cons,[car,'x'],['append',[cdr,'x'],'y']]]]]
);

> [append,[_,[a,b]],[_,[c,d]]]
[a,b,c,d]
> [append,[], [_,[c,d]]]
[c,d]

函數:[pair,x,y]取兩個相同長度的表,返回一個由雙元素表構成的表,雙元素表是相應位置的x,y的元素對.
LispScript.Run(
 [defun,'pair',['x','y'],
  [cond,
   [[and,[isNull,'x'],[isNull,'y']],NIL], 
   [[and,[not,[atom,'x']],[not,[atom,'y']]],
    [append,[[[car,'x'],[car,'y']]],['pair',[cdr,'x'],[cdr,'y']]]
   ]]]
);

> [pair,[_,[x,y,z]],[_,[a,b,c]]]
[[x,a],[y,b],[z,c]]

[assoc,x,y]取原子x和形如pair函數所返回的表y,返回y中第一個符合如下條件的表的第二個元素:它的第一個元素是x.

LispScript.Run(
 [defun,'assoc',['x','y'],
  [cond,[[eq,[car,[car,'y']],'x'],[car,[cdr,[car,'y']]]],
   [[isNull,'y'],NIL],[true,['assoc','x',[cdr,'y']]]]]
);

> [assoc,[_,x],[_,[[x,a],[y,b]]]]
a
> [assoc,[_,x],[_,[[x,new],[x,a],[y,b]]]]
new

[ret,e]返回運算式計算結果
LispScript.Run(
 [defun,'ret',['e'],[car,['e']]]
);

[str,e]返回運算式計算結果的引用
LispScript.Run(
 [defun,'str',['e'],[_,[_,'e']]]
);

我們來看一下為什麼要定義ret函數:
我想通過前面的解釋和實際應用大家已經理解了引用(quote)的重要性,並且很容易證明:[[_,e]] = [e]
現在的問題是我們必須要定義一個引用的反函數f,令[f,[_,e]] = e
而顯然地ret正是這樣一個函數

[map,x,y]期望x是原子,y是一個表,如果[assoc,x,y]非空返回[assoc,x,y]的值否則返回x
LispScript.Run(
 [defun,'map',['x','y'],
  [cond,[[isNull,[assoc,'x','y']],'x'],[true,[assoc,'x','y']]]]
);

[maplist,x,y]期望x和y都是表,返回由x中的每個元素t求[map,t,y]的結果構成的表
LispScript.Run(
 [defun,'maplist',['x','y'],
  [cond,
   [[atom,[_,'x']],[map,'x','y']],
   [true,[cons,['maplist',[car,[_,'x']],'y'],['maplist',[cdr,[_,'x']],'y']]]
  ]
 ]
);

因此我們能夠定義函數來串連表,替換運算式等等.也許算是一個優美的標記法, 那下一步呢? 現在驚喜來了. 我們可以寫一個函數作為我們語言的解譯器:此函數取任意Lisp運算式作自變數並返回它的值. 如下所示:


LispScript.Run(
 [defun,'_eval',['e','a'],
  [ret,[maplist,[_,'e'],'a']]
 ]
);

_eval.的簡潔程度或許超出了我們原先的預想,於是這樣我們獲得了LispScrip實現的一個完整的自身的解析器!
讓我們回過頭考慮一下這意味著什麼. 我們在這兒得到了一個非常優美的計算模型. 僅用quote,atom,eq,car,cdr,cons,和cond, 我們定義了函數_eval.,它事實上實現了我們的語言,用它可以定義和(或)動態產生任何我們想要的額外的函數和各種文法(這一點比較重要)

其他(略為複雜)的擴充:

下面我們定義變數的賦值操作[setq,paraName,paraValue]
LispScript.Run(
 [defun,'setq',['para','val'],
  [ret,[defun,'para',[],[_eval,'val']]]]
);


增加邏輯操作符or,[or,x,y]返回t如果它的自變數有一個為t,否則返回[]
LispScript.Run(
 [defun,'or',['x','y'],
  [not,[and,[not,'x'],[not,'y']]]]
);

增加迴圈控制foreach,[foreach,v,[list],[expr]]
foreach期望list是一個表,依次取表中的每一個原子作為expr的參數進行計算,返回計算結果的表
LispScript.Run(
 [defun,'foreach',['v','list','expr'],
  [cond,
   [[isNull,'list'],[]],
   [true,[cons,[_eval,[_,'expr'],[['v',[car,'list']]]],['foreach','v',[cdr,'list'],[_,'expr']]]]
  ]
 ]
);

增加批量賦值操作let,[let,[[a1,v1],[a2,v2]...]]
LispScript.Run(
  [defun,'let',['paralist'],
   [foreach,"'v'",'paralist',[_,[setq,[car,"'v'"],[car,[cdr,"'v'"]]]]]
  ]
);

總結

現在該回過頭來看看我們究竟做了什麼,以及這麼做有什麼意義了。
首先我們用javascript實現了一個簡單的向下遞迴的詞法分析器,它能對嵌套數組的每個原子進行簡單處理,加上幾個輔助函數(toEvalString(),Assert(),Element()和一個存放函數名稱的堆棧...簡單來說我們僅用了數十行代碼實現了一種全新的“函數式”語言??LispScript的完整核心。
接著我們定義了7種原始操作,它們分別是quote,atom,eq,car,cdr,cons和cond
然後(相對較複雜地),我們定義了三種用來描述和調用函數的標記,它們分別是lambda, label以及defun,於是我們成功地用另外不到百行代碼實現了LispScript語言的核心環境。
接著(接下來的部分已經可以完全獨立於javascript)我們用7種原始操作符和函數定義標記defun定義出一些新的函數,分別是:isNull,and,not,append,pair,assoc,ret和str
然後我們驚喜地發現,可以僅用一行LispScript指令定義出自身的“解析器”??_eval函數
最後我們在此基礎上定義出一些略為複雜的函數,它們包括:or,setq,foreach和let,其中一些新函數帶給我們的新語言定義變數和處理迴圈的能力,加上前面實現的一些函數,一個比較完善的基礎環境就搭建成了。

寫在最後:LispScript和Lisp

事實上我們依照[ref. Paul Graham.]的精彩描述用javascript實現了LispScript,毫無疑問,它是一種Lisp(或者Lisp風格的函數式語言),儘管功能上還十分簡陋,但它確實是符合Lisp的基本思想和擁有Lisp的基本特性。由於javascript數組文法的特點,我用[]取代了[ref. Paul Graham]中的(),用逗號取代了空格作為分隔字元。同[ref. Paul Graham]的文章以及目前一些標準(或者相對標準)的Lisp不同的是,我根據javascript靈活的特點有意弱化了LispScript的文法結構,這樣使得LispScript更加靈活,也更加方便實現,然而代價是一小部分的可維護性和安全性。
最後,LispScript還有許多需要完善的內容,例如,最明顯地是它基本上還不具有基本的數值運算能力(相對而言,符號操作能力已經比較完善),另外對原子巨集指令引數合法性的檢驗、副作用, 連續執行 (它得和副作用在一起才有用), 動態可視域、複雜資料結構支援以及注釋文法(這相當重要!)也都是它所欠缺的,不過這些功能“都可以令人驚訝地用極少的額外代碼來補救”。

感謝約翰麥卡錫,這位天才早在數十年前就向我們展示了一種程式設計領域內至今無人能超越的“極致的美”,他於1960年發表了一篇非凡的論文,他在這篇論文中對編程的貢獻有如歐幾裡德對幾何的貢獻.1 他向我們展示了,在只給定幾個簡單的操作符和一個表示函數的記號的基礎上, 如何構造出一個完整的程式設計語言. 麥卡錫稱這種語言為Lisp, 意為List Processing, 因為他的主要思想之一是用一種簡單的資料結構表(list)來代表代碼和資料.

感謝保羅格雷厄姆,他用淺顯易懂的語言將Lisp的根源和實質展現在我們面前,令我們能夠幸運地零距離體驗Lisp的這種“超凡的美”

如果你理解了約翰麥卡錫的eval, 那你就不僅僅是理解了程式語言曆史中的一個階段. 這些思想至今仍是Lisp的語義核心. 所以從某種意義上, 學習約翰麥卡錫的原著向我們展示了Lisp究竟是什麼. 與其說Lisp是麥卡錫的設計,不如說是他的發現. 它不是生來就是一門用於人工智慧, 快速原型開發或同等層次任務的語言. 它是你試圖公理化計算的結果(之一).

隨著時間的推移, 中級語言, 即被中介層程式員使用的語言, 正一致地向Lisp靠近. 因此通過理解eval你正在明白將來的主StreamCompute模式會是什麼樣.

References
The Roots of Lisp Paul Graham. Draft, January 18, 2002.
LISt Primer Colin Allen & Maneesh Dhagat.Tue Feb 6, 2001.(http://mypage.iu.edu/~colallen/lp/lp.html)



聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.