More Related Content
Similar to 「Lispインタープリター」勉強会 2014.12.04 (20)
「Lispインタープリター」勉強会 2014.12.04
- 1. ウルシステムズ株式会社
http://www.ulsystems.co.jp
mailto:info@ulsystems.co.jp
Tel: 03-6220-1420 Fax: 03-6220-1402
「Lispインタープリター」勉強会
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
2014/12/03
講師:近棟稔
説明に利用するソースコードの場所は以下の通りです
[短縮URL]
http://goo.gl/wtkXro
[URL]
https://22662085c43898e6b7217dd85e8ec1a5e8bb286f.google
drive.com/host/0B3XsaTcJZ4A3ZmpfNXZyZmZqR0U/
- 2. はじめに
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
1
「プログラマーなら一度はLispを作る」という言葉は有名です。ですが、実際に作ったことがある
人はあまり居ないようです。理由として「このような技術は実務には関係ない」とか、「作ろうと
思っても、簡単に実現可能なほど、まとまった情報が無い」などが考えられます。
この勉強会では、プログラミング言語としてJavaScriptを用い、実際に動作する、古風で小さ
なLispインタープリターを、段階を追って作成します。
JavaScriptによるLispインタープリターの実装は、最初は22行で実装した四則演算インタープ
リターの説明から開始します。最終的にはLispのマクロをサポートした、147行のLispインター
プリターに育て上げます。
なお、Lisp言語を知らない人も多いと思われますので、そのような方向けに、Lisp言語の非常
に基本的な部分から説明したいと思いますので、ご安心ください。
Lisp言語の全機能を網羅することはしませんが、この勉強会に参加すれば、Lispインタープリ
ターがどのように動作しているのか、なんとなく分かった状態になることを目指しています。そして、
それが分かれば、プログラミング言語を見る目が少し変わると思います。
- 3. 今回説明するLispインタープリターの由来
Peter Norvig による、「(How to Write a (Lisp) Interpreter (in Python))」を参考にしました。Python
で作成されており、たった113行でインタープリターが実装されています。
http://norvig.com/lispy.html
http://norvig.com/lispy2.html
Peter Norvigは、GoogleにてResearch Directorをやっている人です。また、UdacityというWeb上の
オンラインコースでArtificial Intelligenceなどを教えています。TEDにて公演を行ったこともあります。
(ちなみに近棟もUdacityでNorvigの授業を受けました)
この勉強会で説明するのは、勉強会用に特別に作成したLispインタープリターです。
JavaScriptを用いて実装しました。文法は、Clojureから借用しています。言語の名前はmylispと名
付けました。最終的なコード量は147行になりました。
なお、コードエディター部分に関しては、CodeMirror (http://codemirror.net/) を利用しています。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
2
http://www.ted.com/talks/peter_norvig_the_100_000_student_classroom
- 4. mylispの全体像(147行)
[マクロ展開処理部分(16行)]
ソースコードを前処理する、「マクロ」という機能を提供します。
C言語のマクロと意味的には同じですが、Lispのマクロは非常に強力なため、
ソスコードジェネレータであると捉えたほうが正確です。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
3
[パーサー部分(24行)]
ソースコード文字列をAST(抽象構文木)の構造に組み立てます。
[インタープリターの変数格納用コンテナー(9行)]
変数や関数を格納するためのコンテナーを用意します。
[ASTを読みながら、プログラムの実行を行う部分(34行)]
ASTを読みながら、プログラムを実行します。インタープリターの中核部分です。
[組み込みライブラリー(60行)]
足し算、引き算など、プログラムを作る際に最低限無いと困る処理を
記述した部分です。
- 7. 四則演算インタープリターの使い方
この四則演算インタープリターには、前もって5つの関数が用意されています。新たな関数を新規に定義すること
や、変数を定義することは今のところ出来ません。前もって用意されている5つの関数は以下のものです。
ここで、Javaなどの言語では関数名に使えない「+」などの記号が関数名になっている事に違和感を覚えるかもし
れません。ただ、Lispではしばしばこのような記号を関数名として使用します。ちょっと名前の付け方が違う程度で
あると思ってください。たとえば「add」のような関数名の代わりに「+」という名前の関数を付けます。
四則演算インタープリターを使った計算は、以下のように行います。
+関数をadd関数として記述した場合、
JavaScript的に書くと、add(1, 2, 3)
と同じ
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
6
関数名引数処理内容
+ 数値(可変長引数) 引数の数字の和を計算
- 数値(可変長引数) 引数の数字の差を計算
* 数値(可変長引数) 引数の数字の積を計算
/ 数値(可変長引数) 引数の数字の商を計算
mod 第一引数は分子、第二引数は分母引数の2つの数字の余りを計算
つまり、ある関数を実行したい場合は、
配列の先頭要素が関数名を入れ、
第二要素以降に引数を入れるとします。
関数
+関数をadd関数とし、/関数をdiv関数と記述した場合、
JavaScript的に書くと、add(1, div(1, 2)) と同じ
[関数名, 引数1, 引数2, 引数3]
次ページ以降で、四則演算インタープリ
ターの中身の説明をしていきます。
- 8. 変数や関数のグローバル変数領域:globalEnv
インタープリターに限らず、ほとんどのプログラミング言語で共通することとして、変数や関数を格
納するための空間が用意されています。この空間に、名前をキーとして、値を取り出せるようにした
り、名前をキーとして関数の実装を取り出せるようにしたりします。
このインタープリターでは、そのような空間としてglobalEnvというオブジェクトを用意しています。構
造は単なるHashMapです。名前をキーとして値や関数の実装を取り出せるようになっています。
単純な計算であればglobalEnvの呼び出しでも可能です。
このglobalEnvというHashMapは、以降の説明でも出てきます。globalEnvは、ひとことで言うと「グ
ローバル変数領域」なので、このインタープリターの最終形まで存在し続けます。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
7
+関数をadd関数として記述した場合、
JavaScript的に書くと、add(1, 2, 3) と同じ
mod(5, 3) と同じ
- 10. 四則演算インタープリターの計算の進行例:
入力がevaluate(['mod','5','3'], globalEnv); の場合
evaluate(['mod','5','3'], globalEnv);
↓
'mod'と'5'と'3'がevaluate(x, globalEnv)のxとして代入される。それぞれ以下の様になる。
evaluate('mod', globalEnv) → function(num, div) {return Number(num) % Number(div);}
evaluate('5', globalEnv) → '5'
evaluate('3', globalEnv) → '3'
↓
mod(5, 3)が計算される。より正確には、mod.apply(null, ['5', '3']) が実行される。このapplyとい
う関数の意味は、mod('5', '3') と同じ。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
9
['mod','5','3']
[mod関数の実装,'5','3']
mod関数の実装に引数('5','3')を渡して実行
- 11. 四則演算インタープリターの計算の進行例:
入力がevaluate(['+', '1', ['mod','5','3']], globalEnv); の場合
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
10
['+', '1', ['mod','5','3']]
[+関数の実装, '1', ['mod','5','3']]
[+関数の実装, '1', [mod関数の実装,'5','3']]
[+関数の実装, '1', mod関数の実装('5','3')]
[+関数の実装, '1', '2']
+関数の実装('1', '2')
3
- 13. Lispは1958年に発明された歴史ある言語です
Lispは1958年にJohn McCarthyによって発明されました。時代的にはコンピュータの黎明期にあた
ります。非常に歴史のある言語です。関数型言語であり、なおかつ公式に標準化された最初のオ
ブジェクト指向言語でもあります。
太平洋戦争(3年10ヶ月)
Lispは、プログラムで扱う主たるデータ構造の1つを
リスト構造とし、また、プログラム自身をリスト構造の入れ子
で表現する言語です。プログラムコードはデータなので、
プログラムを機械的に(プログラムによって)変形したり
自動生成したりしやすいという特徴を持ち、ここがLispの
最も大きな強みとなっています。
言ってみれば、Lispは、コードの自動変形・自動生成の
機能を内蔵したプログラミング言語です。
このコードの自動生成機能は、「マクロ」と呼ばれています。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
12
1957
Fortran
John McCarthy
1959
COBOL
(google画像検索より引用)
1946
ENIAC
1936
Alan Turingの
Turing Machine
(仮想機械)
1947
ノイマン型
アーキ
テクチャ
1941
Zuse Z3
世界初の
コンピュータ
1958
LISP
実装
(構想は1956年)
5年5年10年
- 18. evaluate関数が逐次実行「do」を解釈できるようにします。
「do」の導入のためには、do関数をglobalEnvに追加します。
そもそも逐次実行とは??
プログラムはループなどが無ければ、上の行から下の行に向かって順番に実行されます。これが
逐次実行です。「do」の場合は、この逐次実行実行に加えて、最後に計算した計算結果を返すとい
う役割もあります。
「do」の簡単な使い方
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
17
追加部分
上から順にalertを表示し、
doの最後に指定した「5」を
返却している。
[注] Common Lisp では、「do」はループを意味しますが、
Clojureを真似て、mylispでは逐次実行の意味としています。
- 21. 「def」の利用例
「def」を利用し、PIを定義してみます。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
20
PIを定義していない場合、PIを評価しても、
PIというシンボルをそのまま返却します。
PIを3.14159と定義します。内部的には、
globalEnvのHashMapに代入しています。
PIを再度評価すると、今度は3.14159が返ります。
globalEnvに、「PI」というキーで「3.14159」が入っていることが分かります。
- 24. 「fn」の利用例
「fn」によって関数オブジェクトを作成し、そのオブジェクトを「def」によってincrementというキーに保
持させてみます。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
23
JavaScriptにおける
var increment = function(x){return 1 + x;};
このように、
JavaScriptの
関数オブジェクト
へと評価されます。
increment(2)を呼び出してみます。
1 + 2 の計算結果である3 が返ってきます。
しかし、現在の関数の実装では、引数の値を直接globalEnvに代入しているために、
関数実行後もglobalEnvに関数の引数が残ってしまいます。
これを解決するために必要なのは、グローバル変数領域であるglobalEnvのHashMapに
引数を保持させるのではなく、メソッド呼び出しに閉じた、階層化されたHashMapを使う必要があります。
つまり、呼び出し階層が1つ深くなると、その階層専用の領域を持つ必要があります。
- 25. 変数が必ずglobalEnvに代入される事の問題点
現状、何かの値や関数に名前を付ける(defする)場合、必ずglobalEnvに代入を行っています。
このような仕様では、具体的には以下のような問題が発生します。
1. 関数内でdefを使うと、「関数ローカル」な変数を定義しようとしても、必ずglobalになります。
(この挙動は、JavaScriptでvarを付けずに変数定義した場合と似ています。)
2. 関数呼び出し時に渡した値と引数の名前とのマッピングもglobalEnvに登録しているため、関数呼
び出しをするたびに引数で渡した値がglobalEnvに格納されてしまいます。
JavaScriptにおける、このような挙動をさせたい。
つまり、「a」も「b」も1や2を格納していない状態になるべ
き。しかし今はaに1が入り、bに2が入るような挙動をして
しまう。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
24
結局、globalEnvを使った変数のマッピングを
していてはダメで、関数ローカルなマッピングが
必要!
- 27. シンボル(変数名)でオブジェクト(変数や関数)を引き出す仕組みを改善する。
(現状の単純なHashMapから、ローカルスコープを持ったHashMapへ)
単純なHashMapから、ローカルスコープを持ったHashMapへ変更するため、以下のようなデータコ
ンテナを作ります。
このデータコンテナを用いて、以下のような構造を作れるようにします。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
26
globalEnv
env2
env3
env1
a=0
a=1
a=3
globalEnvはグローバル変数領域とし、
env1, env2, env3 はそれぞれ関数
ローカルの変数領域とします。
それぞれの関数ローカルの変数領域
(すなわち、ローカル変数領域)は、
自身の変数領域に該当シンボルが無い
場合、よりグローバル側の環境を探しに
行きます。
- 30. 可変長引数のサポート(余談)
サポートしたい可変長引数の文法
func(a & b) という関数を作り、func(1, 2, 3)という呼び出しをすると、
引数a と引数b に、それぞれ以下の情報が入るようにしたい。
a ← 1
b ← [2, 3]
可変長引数をサポートする前の姿
可変長引数をサポートした後の姿
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
29
← Javaにおけるfunc(int a, int... b) {...} のような意味
mylispでの書き方に従うと・・・
["def", "func", ["fn", ["a", "&", "b"] ]
となる。
「&」が登場した後の引
数に、残りの実引数を
リストとして保持する。
- 33. evaluateの前処理としてプログラムを変形するのがマクロの仕事
今まで見てきたmylispは、ソースコードがJavaScriptの配列表現でした。よって、JavaScriptの配列
操作を行えば、自由に変形することが可能です。この変形操作がマクロの本質です。
マクロ導入前マクロ導入後
['+', '1', ['mod','5','3']]
evaluate ・・・何かに変形・・・
マクロの利用例として、以下のような事が考えられます。
ソースコード上に出現する退屈なコードの記述を、少ない記述で自動生成するため。
DSLを作るため。(DSLを作れば記述を簡素化可能)
パフォーマンスを出すために、既存コードを別の構造に変換するため。
パフォーマンスを出すために、特定の計算をevaluate前に計算済みの状態にしてしまう。
「オブジェクト指向」など、新たな言語上のパラダイムが世の中に登場した際に、言語自身を拡張するため。
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
32
['+', '1', ['mod','5','3']]
evaluate
3
3
マクロ展開
- 35. マクロの利用例
マクロの利用例として、条件分岐の機能を持つ「when」をマクロで実装してみます。
実行例を以下に示します。
実行時には、以下の様にソースコードが変形され、evaluateされます。
["if", "true", [do, ["+", "2", "3"]], "null"]
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
34
["when", "true", ["+", "2", "3"]]
evaluate
5
マクロ展開
ソースコードの変換方法をmacroTableに定義
上のマクロを使って展開した後に実行
- 39. インタープリターの全体処理イメージ
インタープリターは、以下の様な処理フローで処理されます。
Lispで特殊なのは、マクロを用いて一旦プログラムが変形されたり、生成されたりするフェーズがあ
ることです。このようなフェーズの存在は、一般的にはC言語のプリプロセッサーと同一といえば同
一ですが、C言語のプリプロセッサーなどよりも非常に強力なため、Lispを特別なものにしています。
なぜLispのマクロが強力なのかといえば、プログラムがリストの入れ子構造で表現されているため
に、マクロからプログラムコードを扱いやすいという特性によります。
(when true (+ 2 3)) 文字列
["if", "true", ["+", "2", "3"], "null"]
ULS Copyright © 2014 UL Systems, Inc. All rights reserved.
38
["when", "true", ["+", "2", "3"]]
JavaScriptの配列形式(ASTと呼ばれる)
parse
マクロ展開←この処理はソースコードの自動生成といっしょ
evaluate
5