JavaLisp Vol.2 エバリュエータの作成



■リーダとプリンタの説明(村崎担当)

前回は、紙面の関係でリーダやプリンタの説明が出来ませんでしたので、そこから始めます。



●リーダ

現在のリーダは以下のような、複数行の入力に対応していません。次回の記事で対応を予定しています。

QLisp> (defun second (arg-list) (first
QLisp> (rest arg-list)))
second
QLisp>


リーダは、S式を返します。S式はエバリュエータに渡され、評価されプリントされます。その部分は lisp.main()を見れば一目瞭然です。現在リーダはlispクラスのメソッドとして
実装されていて、lispクラスの殆どはリーダ関係のメソッドですので、次回には外部クラスとして切り離す予定です。


reader()
このメソッドは、skipSpace()を呼んでおりこのメソッドの中で1行読みこみ、行バッファに文字列を取得します。そしてカレントの文字を判別してアトムを作成したり、リストを作成したりしています。

skipSpace()
このメソッドはリーダの中でとても重要な役割を持つメソッドです。行バッファよりトークンを切り出すメソッドであると共に、行バッファが空のときは、新しく一行読み込みます。ただし、ユーザが改行するまでブロックします。改行が入力されると、BufferedReaderストリームから文字列を取りこみますが、この時は既にユニコードとして読みこまれます。そして、それを処理しやすいようにchar型のバッファcharBuff[]にコピーします。なおindexOfLineはこのバッファのカレント位置を示しています。

isEscape(), isDigit(), escape()

エスケープ文字と数値文字を判断するメソッドであり、escape()はエスケープ文字が来たときの処理をします。ここではまだ、リストを作る処理だけしか実装していません。具体的には"("が来たら、makeList()関数を呼んでいます。

makeNumber(), makeSymbol(), makeString()
それぞれ数値アトム、シンボルアトム、文字列アトムを作成するメソッドです。シンボルと文字列の違いは、文字列は""または''で囲まれた文字列定数です。それに対してシンボルは変数や関数だと考えてください。

makeList()
リストを作成するメソッドです。このメソッドの特徴は次のようにリストが入れ子になっていた場合、再帰呼び出しとしてreader()を呼び出すことに注目下さい。

(second (quote (a b c))

●プリンタ
プリンタメソッドは非常に単純です。

  static void printer(SExpression se, PrintWriter pw) throws Exception {
    if (se == null)
      return;
    try {
      se.print(pw);
    }
    catch (Exception e) {
      throw e;
    }
  }

SExpressionインターフェイスはprint(PrintWriter)メソッドを持っていて、子クラスは全てこのメソッドをオーバーライドしています。したがって、se変数がシンボルならシンボルクラスのprint()が、数値なら数値クラスのprint()が呼ばれます。リストクラスの場合は再帰呼び出しによって、入れ子になったリストを上手く出力してくれます。



■S式の評価(エバリュエーション)とは(以下 中山担当)

みなさん、こんにちは。エバリュエータの設計と実装は中山が担当します。どうぞよろしく。
さて前回は、Lisp言語の中核となるS式の実装がメインの話題で、ユーザがコンソールから入力したリスト文字列をLispインタープリタの内部的なS式表現としてとりこみ、それをふたたびコンソールにリスト文字列として出力しました。
この場合、たとえば

 (first (first (rest (rest (quote (a b (c d) e f))))))
というリスト文字列(フォーム)をユーザが入力すると、そのまま
 (first (first (rest (rest (quote (a b (c d) e f))))))
という出力フォームがえられます。これが前回までのお話しです。

さて、この記事を読んでいるLisp愛好家の読者のみなさんは、この出力フォームは本来ならばインタープリタによって変形(リスト処理)を受け、単純に


 
という出力がえられるはずだということを既に御存知でしょう。そのインタープリタ内での変形プロセスを逐次的に表現すると、こんなイメージです。

このように、あるフォームを別のフォームに変換してゆくプロセスをLispでは評価(Evaluation)とよび、前回の記事にあわせて表現すれば、
 (first (first (rest (rest (quote (a b (c d) e f)))))) ==> c
となります。

このメカニズムによって、複雑なフォームとして表現された"数式"から、より簡単な値としての"答"を得たり、あるいは評価の過程でコンピュータ内部に副作用を
おこしたり、OSその他のAPIを呼ぶなどして、Lispはプログラミング言語となりうるのでしたね。 そしていよいよ今回は、この評価の仕組み=エバリュエータを実装します。


■エバリュエータには何が必要か?----プログラミング言語的解剖 われわれがこれから作ろうとするエバリュエータとは、上記のイメージによって、

 (1)S式のフォームを入力として、

 (2)それになんらかの法則で変形をくりかえし、

 (3)場合によってはメモリなどコンピュータ内部に副作用を起こし

 (4)結果となる新しいS式を出力する

というオブジェクト---ないしはメソッドだ!....と目標をたてることができます。このような位置付けについては、前回記事の図9も御参照ください。 
それで、問題となるのは、このうち(2)と(3)の部分です。はたして、どんな法則性(評価規則)をもたせれば、このエバリュエータは「Lispインタープリタ」とよびうるものになるのでしょうか? 
ここではそのポイントを、われわれ
の設計スペックとして整理してみましょう。

● QUOTE(引用)---評価の抑制

さて、しばらくLispのおさらいが続きますが、上記のフォームのうち

 (first <s式> )
というフォームのfirstの部分は、右側のS式リストを引数として、その最初の要素を返す、という手続きオペレータを表しています。同様に
(rest <s式> )
というフォームは、引数のS式の最初の要素をとりはらい、残りのリストを返します。上記のフォームは、このふたつが複雑にネスト(入れ子)になったものでした。
以上のように、手続きのコマンドを表現するLispのフォームは

(<手続き名> <引数のS式> <場合によって2番目の引数のS式> ・・)

というS式として一般にあらわされます。そのためエバリュエータは、あるフォームを受け取った場合、最初の要素が手続き名であることを期待して、その手続きにあわせた処理を
引数のS式に対しておこなうのです。
しかし一方、Lispの場合は、データも統一的にS式であらわされます。たとえばエヴァンゲリオンのパイロット名簿なら


(レイ シンジ アスカ)

というリスト表現になります(EvaluatorだからEva、ではありませんが(笑))。この場合「レイ」は手続き名ではありません。このリストを、なんの前置きもなくエバリュエータに渡すと、
エバリュエータはこれを手続きフォームとして処理しようと試み、「レイなんて手続き名は知らないよ!」というエラーを返してくれます。 

したがって、このようにデータとして扱ってほしいリストは、手続きとしての評価をやめてくれるよう、エバリュエータにお願いしなくてはなりません。
そのための仕組みが、上記の式のなかの


(quote <s式>)

というquote手続きです。

エバリュエータはquoteを見つけると、その引数になっているS式に関しては、それを手続きと考えずに、そのままS式を返します。これを「評価の抑制」といいます。
そしてそのS式が、いわば手続きに渡すためのデータになるわけです。

このような抑制メカニズムを持つことで、「プログラムとデータが統一的にS式で表される」という特徴を維持しつつ、プログラミング言語としてのLispが成り立つわけです。だからまずはこのquoteを作らなくてはいけません。


● SETF---変数の束縛

さて、プログラミング言語ならば「変数の宣言と代入」ができなくては話になりません。Lispの場合それはsetf手続きによって実現します。

(setf  a  1)
というフォームは、aという変数の宣言と、その変数へ1という値の代入をおこなう手続きです。このフォームがエバリュエータによって評価されると、インタープリタの内部に変数aのメモリが確保され1という値が代入されます。「リスト処理言語」という触れ込みのLispにおいて、メモリへの記憶という副作用を目的としているという意味では、このsetfは特殊なものです。
しかしこれによって変数がLispでも扱えるようになるので、quoteと同様、エバリュエータの構築に最初に重要な部分です。

この「変数用のメモリの確保」のことをLispでは「変数の束縛」といいます。そして束縛された変数aがあった場合、処理すべきS式として、
a

という(変数名としての)アトムがあたえられると、エバリュエータは今後
1

を返すことになります。
もちろん、さまざまな評価の過程でaの値は書き換えられている可能性もありますが、当然エバリュエータはそのときどきの内容を返してくれなければなりません。なお、変数に束縛されるのはS式全般であり、1というのは数値アトムですから、あくまでもS式です。

たとえば

(setf  エヴァ・パイロット (quote (レイ シンジ アスカ)))
というフォームによって、変数エヴァ・パイロットに(レイ シンジ アスカ)というS式のリストが束縛されます。(リストにはquoteをお忘れなく!)

そして、アトムのうち、変数でない「数値」や「文字列」や「真偽値」は、評価されると自分自身を値として返すことになっているので、この規則ももちろん作っておく必要があります。


●環境(environment)



上記のような変数の束縛を管理してゆくため、Lispインタープリタは「変数名」と「現在の値となるS式」の組をあらわすハッシュを必要とします。これをLispでは「環境」とよびます。ちょっと実装の話を先取りすると、Javaの場合はHashtableが言語機能として備わっているため、このあたりの構築がとても便利なわけです。



●未束縛エラーの補足



もし仮に、束縛されていない変数名を評価しようとすると、それは環境に登録されていないので、エバリュエータは値を返すことができません。そのような未束縛の変数があらわれた場合にエラーを返す仕組みも必要です。これもjavaの場合、Exceptionをエバリュエータに出させて、それをcatchすることで簡単に実現できます。


●プリミティブとDEFUN(手続き定義)



さて、firstやrestのような、リスト処理の基本となるような手続きをプリミティブといい、このプリミティブを組み合わせることで、より複雑な処理を実現してゆくことができます。たとえばリストの2番目の要素を返す手続きは、次の式によって記述可能です。
(first (rest (quote リスト)))
ただし、もちろん実際のLisp処理系には、リストの2番目の要素をとりだすsecondという手続きが備わっています。しかしここで言いたいのは、secondという手続きはfirstとrestというプリミティブの組み合わせで実現可能だということです。実際、first、rest、cons、if、いくつかの述語、+や-、*、/などの算術プリミティブなどをそろえると、あらゆる手続きがプリミティブから記述できるようになる!.....Lisp原書第3版にはそう書いてあります。 

ちなみに、このあたりの話題----なにをプリミティブとして備えれば、あらゆる計算が可能になるのか?という数学的証明---は、「計算可能性の理論」などといってプログラミングサイエンスのマニアックな中核だったりするわけですが、ここでは踏み込む誌面も私の能力もないので、パスすることにしましょう。

とにもかくにも、実際上はプリミティブだけを用いてプログラミングをするということはまず考えられませんから、あらかじめプリミティブを組み合わせた、より高度で便利な手続きをユーザに提供するのもインタープリタの重要な役割のひとつといえます。
しかしそれら高度な手続きは、とりあえずのエバリュエータづくりには不要ですから、今回はパスすることにしてプリミティブだけを作ります。


そして....次が一番重要なポイントですが、ユーザが自分自身でも、それらインタープリタから提供される手続きを利用しつつ、さらに複雑な自分向けの手続きを定義できなくてはプログラミング言語としての意味がありません。...というより、まさしくそのプロセスこそLispプログラミングの醍醐味だといえるでしょう。

このような手続きの定義のためには、defunを使います。つまり

(defun  <手続き名> (引数) (手続きの内容))
というフォームによって、新しいオリジナルな手続きの定義と使用が可能になります。たとえば先のsecondという手続きは、
(defun second (arg-list) (first (rest arg-list)))

と記述でき、このフォームをエバリュエータが評価するとインタープリタは手続き名と手続き内容を環境に覚えます。そして次回からは
(second (quote (a b c))
に対して
b

を返してくれるようになります。うーん、素晴らしいですね!....ぜひこのdefunの評価メカニズムを、われわれのエバリュエータでも作ることにしましょう。
それができると、同じ仕組みで、あらかじめ高度な手続きを組み込んでゆくことも可能になります。



●再帰的なエバリュエータの呼び出し



これは特にあらためて話題にするまでもないほどLispの根本的なコンセプトですから説明は端折ります。リストがネストされたリスト、というS式の構造に沿いながらS式の解析や評価をするためには、再帰的に自分を呼び出せる仕組みは不可欠です。




■エバリュエータの設計



では、いよいよエバリュエータの設計にとりかかりましょう。 以上のようなエバリュエータの機能の実現にあたっては、

「エバリュエータをそのままクラスとして作る」

「いくつかのクラスが連動してエバリュエータの機能をはたす」(典型的にはEvalとApply)

「S式のメソッドとしてエバリュエータ機能を追加し、S式に自分で評価させる」


などの、いくつかの実装方針がありえます。

特に最後の方式は再帰的なLispの特徴をうまく利用していて、なかなか魅力的です。しかし今回は、エバリュエータをそのままクラスとして作ってみました。理由については最後に述べたいと思います。

なお、今回は、上記に列挙した手続きのうちのquoteとsetf、プリミティブとしてfirst、rest、consを実装しました。重要性を力説したdefunは残念ながら時間の都合で次回のお楽しみとさせていただきます。....竜頭蛇尾でスミマセン。


クラスとしてEvaluatorを作るのですから、その内部に、S式フォームを入力として変形をほどこしS式を出力するメソッドが必要になりますので、このメソッド名はeval()としました。またこのメソッドは設計方針で述べたように、変数の未束縛などで評価に失敗した場合にExceptionを出させる必要があります。また、評価のためには環境となるハッシュが必要となります。

以上よりEvaluatorクラスは、抽象表現として、

ファイル:Evaluator.java

Class Evaluator {
	public EvaluatorI ();
	public SExpression eval ( SExpression form, Hashtable environment) 
			throws Exception;	
}
と設計できます。そしてインタープリタ本体となるlisp.javaからは、次のようにリーダー部とプリンター部のあいだで呼び出します。

ファイル:lisp.java
SExpression form;
Hashtable environment = new Hashtable ();
Evaluator evaluator = new Evaluator();

for( ; ; ) {
	リーダー部によるformの読み込み;
	try {
		SExpression evaluatedForm = evaluator.eval (form, environment);
		プリンター部によるevaluatedFormの出力;
	} catch (Exception e) {
		エラーメッセージの出力;
	}
}

以上によって、エヴァリュエータがインタープリタに組み込まれ、無事にLispとしての機能を果たすことができそうですね。

次はいよいよEvaluator.javaの実装です。





■Evaluator.java


設計方針まで丁寧にみてきたので、だいたいの感じはつかんでいただけたと思いますから、実装についてはソース(リスト4)を実際にみていただきながら、ここでは要点だけを説明します。


●EvalとApplyの協調による評価プロセス

一番重要なポイントは、eval()単体で評価を進めずに、もうひとつのメソッドapply()をEvaluator内部に作って、両者の協力によって評価を進めるようにしている点です。その評価アルゴリズムは次のステップに要約できます。

(1)評価対象フォームがアトムの場合はeval()自身が直接評価を行う。
 (1-1)アトムのうち数値・真偽値・文字列の場合はそのままフォームを値として返す
 (1-2)アトムのうち変数は、環境のハッシュを調べて、束縛されていたらその値のS式を戻し、未束縛の場合はエラーを戻す

(2)quoteやsetfなどの、特殊な引数評価が必要なフォームはEval()自身が評価する

(3)それ以外のフォームについては、フォームの全引数を再帰的に自分で評価したうえで、手続き名と評価した引数のリストをapply()へ渡す

(4)apply()はfirstやrestなどの基本プリミティブを直接評価する

(5)apply()は受け取った手続きがシンボル---すなわち手続き名(プリミティブ以外の)ならば、手続きの属性リストを調べて、その記述にそって処理をする

(6)それ以外のときapply()はEval()に適当な処理を渡す


このような再帰的な評価プロセスの構造は、評価すべき対象フォームのネスト構造にしたがって自然におこなわれるので、どんなフォームであっても、変数の束縛や手続きの未定義などのエラーに出会わないかぎり、かならず評価が達成されます。
これはソースをみてもらい、できれば実際に動かしてもらえると、エバリュエータの動作の実感がつかめると思います。

なお、今回のソースではdefunの未実装と当時にapply()の一部(上記ステップの5と6)が未実装ですが、いちおう次回に完全なものにする予定です。今回の場合は、定義済みのプリミティブ以外はすべて手続き未定義エラーを戻すことでお茶を濁しています。はい。

なお、上記のアルゴリズムを読んで、「うーん、どこかで聞いたことがあるアルゴリズムだ」とピンときたあなた。かなりのLispマニアですね!! 実は、以上のアルゴリズムは、Lispの聖典ともいうべきウィンストン&ホーン『Lisp原著第3版』(培風館、1982年)の第18章「Lispで書くLisp」を、全くそのまま踏襲したものです。

その説明においては、Lispインタープリタの評価手続きの様子がLisp自身をメタ言語として記述されています。そしてこのEvaluator.javaは、それをそのままjavaで書き直したものになっているのです。
この本をお持ちの方は、ぜひそのmicro-evalとmicro-applyを、このjava版のeval()とapply()とで比べてみてください。

話はそれますが、「Lispで書くLisp」----言語的メカニズムはさておき、このような呪文的な美しい響きが、Lispがマニアックな賛美を集める一端かななどとも思ったりもするのですが....そのあたりは、先月村崎さんが「Lispは言語の女王様」というくだりでも述べていますね。 私なども、ひさしぶりにこのような本をひっぱりだしてくると、昔秘かにあこがれていた女の子の写真がのっている卒業アルバム
を久々に見たような、なんとも甘酸っぱい憧憬というか感傷というか.....があったりします。

リスト4

--------------------------------------------------------
//タイトル:  製品名
//バージョン:  
//著作権:      copyright (c) 1999
//作者:        T.Murazaki & M.Nakayama
//会社名:      MN
//説明:       内容
package quilt;

import java.util*;

public class Evaluator {

  public Evaluator() {
 }

public SExpression eval(SExpression from,Hashtable environment) throws Exception{

  /*評価対象フォームがアトムの場合*/
  if(form.isAtom()) {

    java.lang.string formClassName = form.getClass().getName();

    /*数値・真偽値・文字列の場合はそのまま戻す*/

    if(formClassName.index0f("Integer")>0){
      return form;
    }
  
    else if(formClassName.index0f("Real")>0){
      return form;
    }


    else if(formClassName.index0f("Nil")>0){
      return form;
    }   

    
    else if(formClassName.index0f("string")>0){
      return form;
    }  

    /*変数の場合、環境(束縛状態)を調べて束縛値または未束縛エラーを戻す*/

    
    else if(formClassName.index0f("symbol")>0){
      java.lang.string symbolName = ((symbol)form).getstr();
      SExpression symbolValue = (SExpression)(environment.get(symbolName));
      if(symbolValue==null){
        throw new Exception("error: unbound variable - "+ symbolName);
      }
      return symbolValue;
    }
    
    else{
      throw new ClassNotFoundException();
    {

   }

    /*評価対象のフォームがリストの場合*/
    else{

      java.lang.string procedure = ((symbol)(((List)form).get(0))).getste();

       /*quote文やsetf文などの、標準的でない引数評価が必要なスペシャルフォームは
   適当な引数だけを評価し、必要な計算を自分で行って結果を戻す*/

    if(procedure.equals("quote")){
      return (SExpression)(((List)form).get(1));
    }

  else if(procedure.equals("setf")){
       java.lang.string symbolName = ((symbol)(((List)form).get(1))).getste();
      SExpression binding = (SExpression)(((List)form).get(2));
       try{
          SExpression symbolValue = eval(binding,environment);
          environment.put.(symbolName,symbolValue);
          return (SExpression)(environment.get(symbolName));
      }
       catch(Exception e){
         throw e;
       }
      }

     /*それ以外の場合はフォームの全引数を自分で評価して、手続き名と評価した引数のリストをApplyメソッドに渡す*/

    else{

     List argumentList = new List();
     try{
       for(int i=1; i<((List)form).size();i++) {
         argumentList.add(eval((SExpression)(((List)form).get(i)),environment));
       }
     }
     catch(Exception e){
         throw e;
       }
     }
      try{
       return apply(procedure, argumentList);
      }
      catch(Exception e){
         throw e;
       }
     }
  }
  /*リストの場合終わり*/

}//eval()終わり

public SExpression apply(java.lang.string procedure, List arguments) throws Exception{ 


 /*first文、rest文、cons文などの基本プリミティブは直接処理する*/

  if(procedure.equals("first")) {
    SExpression arg = (SExpression)arguments.get(0);
    if(arg.isAtom()){
      throw new Exception("errer:bad argument type - "+((symbol)arg).getstr());
    }
    return (SExpression)(((List)arg).getFirst());
  }

 else if(procedure.equals("rest")){
    SExpression arg = (SExpression)arguments.get(0);
    if(arg.isAtom()){
      throw new Exception("errer:bad argument type - "+((symbol)arg).getstr());
    }
    List returnForm = new List();
    for(int i=1; i<((List)arg).size();i++) {
      returnFrom.add(((List)arg).get(i));
   }
    return (SExpression)returnFrom;
 }

 else if(procedure.equals("cons")){
    SExpression newFirst = (SExpression)(arguments.get(0));
    SExpression oldForm = (SExpression)(arguments.get(1));
    if(oldForm.isAtom()){
      throw new Exception("errer:bad argument type - "+((symbol))oldForm.getstr());
    }
    List returnForm = new List();
    returnFrom.add(newFirst);
    for(int i=0; i<((List)oldForm).size();i++) {
      returnFrom.add(((List)oldForm).get(i));
   }
    return (SExpression)returnFrom;
 }

 /*非プリミティブの手続き名の場合は、
 手続きの記述を環境から取り出して処理する*/
 /*ただし,今回は未実装のため、上記のプリミティブ以外は未定義エラーを戻す*/
 else {
  throw new Exception("errer:underfined function - "+ procedure);
 }

}
//apply()終わり


}


●javaで楽々

さて、このjavaによる実装にあたっては、環境となるハッシュはHashtableクラスをそのままつかい、Hashtableは値としてオブジェクトなら何でももてますから、S式をそのままもたせることで変数の評価がシンプルに実現されています。

また、メソッドの再帰呼び出しは問題なくjavaでサポートされていますし、エラーの処理もExceptionのメカニズムで非常にシンプルになっています。 さらに、S式の実装に使用したJava2から登場のLinkedListクラスにはgetFirst()などのリスト処理メソッドが備わっているため、プリミティブのリスト処理も、それらのメソッドをそのまま適用することが出来ています。ソースをながめて、「うーん、これって、
ほとんど単にLinkedListのラッパーじゃないの?」と感じたあなた、正解です。javaによって、Lispインタープリタは、ここまで楽に書けるのですね。(まあ「javaで楽々」というのはLispに限りませんが.....)




●クラスかメソッドか?

最後に、最初のほうで述べた「エバリュエータをS式のメソッドとして作り、S式に自分で自分を評価させる」という方法について、ちょっと考えてみたいと思います。
上記の6つのステップのうち、最初のアトムの評価などでは、クラスに応じた処理が多いため、このような場合、あきらかにS式のメソッドにしたほうが効率的です。S式のインタフェースとしてeval()を宣言し、各派生クラスで、そのクラスに応じた評価処理としてeval()をオーバーライドするという戦法です。

そして評価対象のフォームはS式のネストになっていますから、末端のS式から自分を評価してくれば、自然と最後にはフォーム全体の評価が無事に得られることになります。今回でも実際、最初のエバリュエータのバージョンはそのように作っていました。

しかし実際にはListクラスでのeval()のみが肥大化して、eval()を独立したクラスで作るのとほとんど変わらなくなってしまい、そのわりに評価の仕組みが見えづらくなるため、とりあえず今回はこのような単体のクラスとしました。このあたりはまだ考慮の余地があるため、今後の進行にともない、また変更があるかもしれません。リアルタイムに連載しながらの開発なので、そのあたりは御容赦いただくとともに、逆に筆者たちの紆余曲折を楽しんでいただくのも一興かな(?)と思います。



■次回の予定


まだ足りない部分も多い状態ですが、今回とりあえずエバリュエータを実装したことにより、なんとかLispインタープリタとよべるものの最初の状態になりました。
図2にその実際の動作の様子を示しています。いちおう無事にLispとして期待される挙動は得られていますね。

次回は、やり残しているdefunや、もう少しプリミティブを増やすなどして、Lispインタープリタとして一応の完成までもっていきたいと考えています。そして次々回で画ンドウと描画手続きを加えてLOGO風に発展させ、次第にシステムの名前である「QUILT」を描けるようなグラフィクシステムの方向に持っていくつもりすので、どうぞ御期待ください。


QLisp> 1
1
QLisp> a
error: unbound variable - a
QLisp> (quote a)
a
QLisp> (quote (a b c))
(a b c)
QLisp> (setf a 1)
1
QLisp> a
1
QLisp> (setf alist (quote (a b c)))
(a b c)
QLisp> alist
(a b c)
QLisp>(setf blist (quote (x y z)))
(x y z)
QLisp>(first alist)
a
QLisp>(rest blist)
(y z)
QLisp>(cons alist blist)
((a b c) x y z)
QLisp>(cons (first alist) (rest alist))
(a b c)
QLisp>quit
Bye Bye - Qlisp

前にもどる