木の実を埋めなかったので拾われないLisp
TL; DR
Lispをつくろうとして失敗しました。ちーん。
どんなものをつくろうとしたか
Common Lispのすごく小さなサブセットをつくろうとしました。それをつくることで、普段使って理解した気になっているCommon Lispのパッケージやリードテーブル、ひいては実行モデル等を理解するのが目的でした。
機能としてはなんとなく、以下のようなことを妄想していました:
- Lisp-2
- CLOSなし
- コンディションなし
- loopなし
- 文字列あり(Unicode文字列)
- リストあり
- 関数よびだしあり
- パッケージあり
- リードテーブルつきのreadあり
- したがって簡単なリーダマクロ
- レキシカル環境あり
- eval
- マクロ展開
実際にはどんな産物ができたか
これです。
機能的には
- Lisp-2
- 組込み関数あり
- ユーザ定義関数はなし
- リードテーブルなしのread
- 関数をちゃんと作れなかったので
- パッケージあり(切り替えられないけど)
- いちおう簡易printがある
- 環境の構造がちょっとおかしい?
- REPLがある
関数呼び出しが実装できない気がして気が遠くなってきたので、いったん一区切りつけることにしました。
敗因はなんだったのか
環境(グローバル/レキシカル)やパッケージ、そしてシンボルのスロットについての理解が誤っていたことが原因でした。データ構造の設計に誤りがあるのです。
nutslispではパッケージと環境はそれぞれ以下のように定義されています。
type LispPackage* = ref object of LispT name*: string nicknames*: seq[string] environment*: LispEnvironment LispEnvironment* = ref object of LispT parent*: LispEnvironment binding*: TableRef[LispObjectId, LispSymbol]
「パッケージがグローバル(トップレベル)環境である」「パッケージはシンボルのテーブルをもつ」という認識と、「環境はその親環境を持ちうる(レキシカル環境の一つ外の環境)」「シンボルのvalueスロットに値を持つ(これは環境においてもそうするものだ)」という認識で、この構造にしました。ちなみに、Nimの言語上の制約から、自前定義した型(クラス)をテーブル(Nimにおけるハッシュテーブル)のキーにすることができません。そういった事情もあり、パッケージが保持するシンボルテーブルも兼ねて、binding
がLispObject
のIDからシンボルへのテーブルになっています。
でも、これでは(レキシカル)環境のシンボルに束縛した値を得るときどうするのでしょう。シンボルのスロットには、パッケージのトップレベルの値が入っているはずです(たとえば(setf hoge "mojiretsu")
としたときの値)。シンボルをレキシカルな環境の値や関数保持用の構造として利用すると、トップレベルの値が上書きされて消えてしまいます。
さあさあ、てえへんだ。
おわりに
正しくはどうあるべきか、ひいては環境やパッケージやシンボルとは何であるのか、についてはまだ不明です。Hyperspec読書大会を引き続きひとり開催してなんとか理解を深めたいところです。
おそらくありがちなところで盛大に転んでしまったというところなんでしょう。目標の、リードテーブルや関数の実行モデルやあれやこれやの理解を深めること、はまだまだ先が長そうですね。
インタラクティブシなREPLをWebページ上に実装する
自分で言語を実装したとき、できればWebページ上でさっと試せるとカッコいいなと思いますよね。たとえばこんな感じで:
- http://golang-jp.org/
- https://islisp.js.org/
- https://jscl-project.github.io/
- https://www.haskell.org/
今回ぼくは自作の言語に対して、同じようなことを実験したので、その成果を記します。
どんなものを作ったか
どうやって実装したか
ここではテキストエリアのonChangeなどの更新をさっと取得・描画できるVue.jsを用いたリアクティブなスタイルで実装していきます。
REPLのデータモデル
まず、REPLに必要なものを考えてみましょう。REPLは三つの要素から出きていると考えました。それすなわち
- 読み込み行
- 出力行
- 過去の読み込み行
の三つです。
読み込み行は、プロンプトを表示し、ユーザの入力を促す行です。
出力行は、ユーザの入力を評価した結果を表示する行です。
過去の読み込み行は、プロンプトと、過去にユーザが入力した文字列から成ります。これは出力行と考えてもよいのですが、なんとなく分けています。
各行をJavaScriptのオブジェクトで表現し、配列で保持しましょう。すなわち、こうです:
[ { type: 'old-prompt', msg: '$ ', input: 'aaaaa'}, { type: 'output', msg: 'aaaaa'}, { type: 'old-prompt', msg: '$ ', input: 'bbbbb'}, { type: 'output', msg: 'bbbbb'}, { type: 'prompt', msg: '$ ' }, ]
REPLのView
そして、あとはそれを描画するViewの部分です。Vue.jsの描画対象を#app
とすると、その要素の中に、データモデルの各type
に対応する要素を書き、v-for
で配列全体を描画する、という流れになります。
<div id="app"> <!-- ここで、各行を描画--> <div v-for="line in lines"> <!-- 出力行 --> <div v-if="line.type == 'output'">{{ line.msg }}</div> <!-- 読み込み行 --> <!-- 入力中の内容はv-modelで逐次取得する --> <div v-if="line.type == 'prompt'"> <div>{{ line.msg }}</div> <input type="text" v-model="readline" v-on:keydown.enter="rep(line.id)"> </div> <!-- 過去の読み込み行 --> <div v-if="line.type == 'old-prompt'"> <div >{{ line.msg }}</div> <span>{{ line.input }}</span> </div> </div> </div>
REPLのcontroller
表示の制御は、REPLのinput
でエンターキーが押されたときに駆動するようにします。REPLなので。
すなわち、JavaScriptのコード全体としてはこんな感じになります。
<script> const app = new Vue({ el: '#app', data: { readline: "", lineCount: 0, lines: [], }, methods: { // 現在の読み込み行の内容でreadとevalをし、 // 過去の読み込み行に更新し、結果と次の読み込み行を追加する rep: function (id) { this.lines[id].input = this.readline.toString() this.lines[id].type = 'old-prompt' // これらはもちろん、あなたの言語のreadとevalですよ! let result = eval(read(this.lines[id].input)) this.print(result) this.prompt() }, // 読み込み行を追加する prompt: function () { let prompt = getCurrentPackageName() + '> ' this.readline = '' this.lines.push( { id: ++this.lineCount, type: 'prompt', msg: prompt }) }, // 出力行を追加する print: function (str) { this.lines.push( { id: ++this.lineCount, type: 'output', msg: str }) } }, created: function () { this.print('welcome to my cool REPL.') this.prompt() } } </script>
あとはCSSでカッコいい見た目や色を設定してあげるだけである。
おわりに
これにより、自作言語ができたときあなたはすぐにWeb REPLを追加することができるようになりました。
まさかLisp以外の記事が増えるとは…。
Nimに入門して簡単なアプリケーションを書くまで
TL; DR
Nimに入門してアプリケーションをつくるまでの道筋を書きました。
Nimってどんな言語?
NimはPythonっぽい見た目をもつ、コンパイラ言語です。静的型付きで、ネイティブコードを吐き、あといちおうメタプログラミングもできるという言語です。
このslashdot.jp (現スラド)の記事「注目を集め始めるプログラミング言語「Nim」」を当時見て興味を持ちました。
書いてみた感想としては、なかなか書きやすくていい言語です。Pythonだと思って書くには、だいぶC言語の香りが強い感じがしますので、注意。あと、辞書(連想配列)はありませんので、注意(正確には、tablesモジュールにありますが、Pythonの辞書ほど手軽には使えません)。NimのコンパイラはC言語を経由してネイティブコードを吐きますが、実行可能バイナリのサイズがけっこう小さくて、速い。こっそりとJavaScriptなんかにもトランスパイルできたりします。
あと、メタプログラミングできるのですが、マクロはあまり書きやすくないなあという印象です(S式ではないため)。
導入のしかた
導入するにはaptで入れるか(きっとhomebrewにもあるのでしょう)、ソースからビルドするかの二通りがあります。ここでは、バージョンも選びほうだいな上、コンパイラ自体のソースコードにデバッグ文を仕込めたりして楽しいため、ソースからビルドしましょう。
1.ソースコードを取得する
ソースコードはgithubから取得します。バージョンでタグを切ってあるので、現時点での最新安定版をチェックアウトします。
$ git clone https://github.com/nim-lang/nim.git $ cd nim $ git checkout v0.17.2
2. ビルドする
ビルドはわりとすぐに終わります。Nimのコンパイラをビルドしたら、Nimの管理ツールのkochをNimコンパイラでビルドして、その後Nimのパッケージマネージャなんかをkochでビルドします。
$ sh ./ci/build.sh ... $ ./bin/nim c koch ... $ ./koch tools ... $ ls ./bin nim nimble nimsuggest
3. PATHを通す
ビルドが終わったら、/path/to/nim/bin
にパスを通しましょう。ぼくはよく~/opt
にNimのリポジトリをクローンして、~/opt/nim/bin
にパスを通してます。
$ echo 'export PATH=$PATH:/home/user/opt/nim/bin' >> .profile
以上で導入完了です。
Nimの基本を覚えるために
Nimの文法や言語の要素を覚えるために、ぼくが読んだのは以下のドキュメントです。
チュートリアル系
ぼくは、公式チュートリアルは3回くらい通して読みました。分量がけっこうありますが、Nimの全ての要素が詰まっているので、何回でも読みましょう(ぼくは今でもお世話になってます)。
マニュアル系
パッケージの定義方法や構成については、NimbleのREADMEが参考になります。
Hello, World!
NimのHello Worldはこんな感じです。
when isMainModule: echo "Hello, World!"
一引数のときに括弧を省略できるので、こんなふうになってます。一引数の関数呼び出しが入れ子になっているときは、左結合的に解釈してくれるのかは謎です。
アプリケーションを作る
で、入門してチュートリアルを4回くらい読んだはいいけど、何をつくってNimを経験したものでしょうか。新たな言語を試すとき、とりあえず一通りの機能を使いそうなアプリケーション、それはLisp処理系ですね。なのでそれを実装しました。
パッケージをつくる
ライブラリパッケージを作成するにはnimble
コマンドを利用します。こんなふうにして、パッケージを作りましょう。
$ mkdir nimlisp $ cd nimlisp $ nimble init ...質問に答える $ ls nimlisp.nimble $ touch nimlisp.nim # このファイルにコードを書く
Lisp処理系を実装する
Lisp処理系を実装するチュートリアルは、本当にたくさんあります。なので、ここではその実装方法を書くようなことはしません。驚くべき処理系を実装したがそれを書くには余白が狭すぎる。
お気に召さなければシェルでも実装してみましょう。
ビルドする
書いたNimbleパッケージは、nimble
コマンドを使ってビルドすることができます。
$ nimble build ... $ ls nimlisp nimlisp.nim nimlisp.nimble
実行するときは、単純に、こうです。
$ ./nimlisp welcome to nimlisp. >
おわりに
ちなみに、こうやって書いたぼくのLisp処理系はこれです。地味にブラウザでも実行できます。
まさかLisp以外の記事を書くことがあるとは…。
Common Lispでシャレオツなアートを描いてみる
はじめに
この記事は、ジェネラティブアートと呼ばれる、なんかコンピュータで生成したっぽいアーティスティックでカッコイイ画像を生成するために悪戦苦闘した、一人のプログラマの記録である。
ジェネラティブアートとは
ジェネラティブ(generative, 生成的)なアート(art, 美術作品)である。Wikipediaの当該項目から引くと、以下のようである:
コンピュータソフトウェアのアルゴリズムや数学的/機械的/無作為的自律過程によってアルゴリズム的に生成・合成・構築される芸術作品を指す。
ふむん。
さらに以下のような特徴を持っているようだ:
コンピュータの計算の自由度と計算速度を活かし、自然科学で得られた理論を実行することで、人工と自然の中間のような、統一感を持った有機的な表現を行わせる作品が多い。
わからぬ。これだけではどんなものかわからぬので、Googleの画像検索してみると、どうやらこのようなアートであるらしい:
要するに、イカしたアートということだ。これは、やってみたい。
然らばやるべし。
ところで、筆者はLISPerである。このようなアートはProcessingでやるのが常套らしいが、Javaふう(というかALGOLふう)の言語とかやっていられなくて挫折した。LISPでやりたい。だから、LISPでジェネラティブなアートをキめてカッ飛ぼうという所存で臨む。
そういう記事である。
Common Lispのジェネラティブアートライブラリ: sketch
ProcessingのLispラッパーといえば、ClojureのQuilがある。Processing自体はJavaで実装されており、そのため同じJVMで動く言語であるClojureは、その機能をフルに利用できるというわけである。
ところで、筆者はCommon Lisp使いである。Clojureが嫌いというわけではない。手慣れた環境であるところのCommon Lispで書けると幸いであり、とてもハッピーであり、脳汁ドバドバなのである。と、いうことで、我が愛するCommon Lispで、Processingっぽい、Quilっぽいことをやってみるのである。
これは意地だ。ただの意地だ。
意地になってそのようなライブラリを探すと、それが案外見つかるもので、正直筆者もビビった。それがvydd氏によるsketchである。READMEはこう書かれてあり:
Sketch is a Common Lisp environment for the creation of electronic art, visual design, game prototyping, game making, computer graphics, exploration of human-computer interaction and more. It is inspired by Processing Language and shares some of the API.
求めていたものまさにこれ感が半端ではない。ぜひこいつを使わせていただこうと思う。
Sketchの導入
以降ではCommon Lispについての基本的な知識はあるものとする。ない読者については、手前味噌ながらこちらの記事「いまから始めるCommon Lisp」を読んでまずCommon Lispに入門してきてほしい。いい言語だよ。
Sketchの導入方法は以下である。また、ジェネラティブアートには欠かせない、パーリンノイズ(後述)のライブラリも併せて導入しておく。
CL-USER> (ql:quickload '(:sketch :black-tie))
Sketchのいろは
Sketchはdefsketch
マクロでスケッチを定義する。そのスケッチの定義時にクラスが生成されるので、そのインスタンスを生成することで、描画プロセスがスタートする。とりあえず四角をいっこ表示するスケッチは以下のコードになる:
CL-USER> (sketch:defsketch first-sketch ((sketch:title "first your sketch") (sketch:width 600) (sketch:height 400)) (sketch:rect 100 100 200 200))
このdefsketch
のbody部分が、毎フレーム毎に呼ばれる描画関数となっている。このbodyを何度呼んでも結果が同じであれば同じ画像が表示され、乱数等の影響によりbodyを呼ぶ毎に数値が変わるとアニメーションになる、という感じである。
ちなみにこのコード、SBCLのREPLに突っ込むとたくさん警告が出るが、無視してほしい。title
とかwidth
とかheight
とかの未使用について怒られるのだ。ちゃんと(declare (ignorable ...))
してほしいものである。
こいつを実際に表示するには、以下のようにする:
CL-USER> (make-instance 'first-sketch)
すると、こういうウィンドウが表示される。
なにをやっているかは、コードを見てだいたい察せることと思う。(x, y)座標が(100, 100)を始点として、幅と高さが200の四角を描いているだけである。
パーリンノイズを可視化する
ジェネラティブアートでは、人工的な部分と自然な部分の中間を狙うものであるらしい。そこで、ここではランダムなんだけど自然な感じを表現するための、パーリンノイズを可視化してみようと思う。
まず、ただの乱数を点の輝度としたものを見てほしい:
CL-USER> (flet ((noise (x y) (random 1.0))) (sketch:defsketch first-sketch ((sketch:title "first your sketch") (sketch:width 300) (sketch:height 300)) (dotimes (x 300) (dotimes (y 300) (sketch:with-pen (sketch:make-pen :fill nil :stroke (sketch:hsb 0 0 (noise x y))) (sketch:point x y))))))
なんというか、砂嵐。ランダムすぎてガチのノイズであって、カオス以外の何者でもない。つらい。
一方で、ケン・パーリンが開発し伝説のディズニー映画『TRON』で使用したというこのノイズ関数は、だいぶ自然である、らしい。どんなノイズなのかを可視化すると、こんな感じ:
CL-USER> (flet ((noise (x y) (normalize (black-tie:perlin-noise (* x 0.1) (* y 0.1) 0) -1 1))) (sketch:defsketch first-sketch ((sketch:title "first your sketch") (sketch:width 300) (sketch:height 300)) (dotimes (x 300) (dotimes (y 300) (sketch:with-pen (sketch:make-pen :fill nil :stroke (sketch:hsb 0 0 (noise x y))) (sketch:point x y))))))
まだ自然っぽく見えないけど、ランダムだけどなだらかであるので、これをテクスチャとかに利用したりすると、自然なものができあがるっぽい。
これを使ってさっそくジェネラティブアートしてみる。簡単には、このノイズを拡大して、円の半径として可視化してみると、それっぽいことがわかった:
CL-USER> (sketch:defsketch mysketch ((sketch:title "perlin circle") (sketch:width 600) (sketch:height 400)) (sketch:with-pen (sketch:make-pen :fill (sketch:rgb 0 0.1 0.1)) (sketch:rect 0 0 600 400)) (let ((interval 17) (noise-factor 0.2)) (dotimes (x (ceiling (/ 600 interval))) (dotimes (y (ceiling (/ 400 interval))) (sketch:with-pen (sketch:make-pen :fill nil :stroke (sketch:rgb 0.2 0.6 0.9)) (sketch:circle (* x interval) (* y interval) (+ (/ interval 4) (* 10 (black-tie:perlin-noise (* x noise-factor) (* y noise-factor) 0)))))))))
なんかシャレオツっぽい。アニメーション(円の半径が変わるとか)しておいてスクリーンセーバーにすると、なんかよさげな気がする。そうするのは読者への課題とする。
もっとジェネラティブっぽさを求めて
もっとノイズを使うといいって本に書いてあった([普及版]ジェネラティブ・アート―Processingによる実践ガイド調べ)ので、もっとランダムやノイズを取り込んでいこうと思う。
たとえば線を引く行為にランダムやノイズを導入して、さらにベジエ曲線にしてみるというのはどうだろう。始点と終点が与えられたとき、その間に制御点を設け、それらをランダマイズして描画するのだ。どうせなら、それを複数回してみるとそれっぽいのでは。
CL-USER> (defun yvalue (sx sy ex ey x) (let ((delta (/ (- ey sy) (- ex sx))) (y0 (/ (- (* sx ey) (* sy ex)) (- sx ex)))) (+ (* x delta) y0))) CL-USER> (defun make-control-points (sx sy ex ey) (let* ((xlis (let (nums) (dotimes (n 2) (setf nums (cons (- ex sx) nums))) (append (list sx) (sort nums #'<) (list ex)))) (ylis (loop :for x :in xlis :collect (+ (yvalue sx sy ex ey x) (- (random 150) 75))))) (loop :for x :in xlis :for y :in ylis :nconc (list x y)))) CL-USER> (let* ((+width+ 600) (+height+ 400) (sx (* +width+ 0)) (sy (* +height+ 0.7)) (ex (* +width+ 1.2)) (ey (* +height+ 0.4)) (rs (make-random-state))) (sketch:defsketch mysketch ((sketch:title "flowline") (sketch:width +width+) (sketch:height +height+) (sketch:copy-pixels t)) (sketch:with-pen (sketch:make-pen :fill (sketch:hsb 0.6 0.9 0.15)) (sketch:rect 0 0 +width+ +height+)) (sketch:with-pen (sketch:make-pen :fill nil :stroke (sketch:hsb 0.374 0.4 0.8 0.04)) (let ((*random-state* (make-random-state rs))) (loop :for n :from 0 :upto 1000 :do (apply #'sketch:bezier (make-control-points sx sy ex ey)))))))
納豆の糸みたいで、それっぽい雰囲気がある。線の数を1000本にして、それぞれのアルファ値を0.15
と少なめにしたため、ちょっとランダマイズしただけの線の集合にジェネラティブアートっぽい雰囲気が出ている。どうも本を見るに、ジェネラティブアートとは、多数のオブジェクトの相互作用っぽさがあれば、それっぽくなるものであるらしい。
Sketchの問題点
しかしながら、ここいらですでになんか問題を感じつつあるのである。
生成した画像を保存できない
ここまででわかる通り、sketchの生成する画像を保存するのには、OSのスクリーンショット機能を利用している。ほかに方法が用意されていないからだ。なので、例えば4000×4000の画像を生成したとして、それを保存するには4000x4000以上の解像度を持つディスプレイがなければならない。
一方でProcessingには画像を保存する関数があるので、そのような問題はない。
色の合成方法を指定できない
色の合成モードには色々なものがあるが、そのうちアルファブレンディングのみをsketchでは利用することができる。たとえば、発光した感じを表現するのにしばしば用いられる加算合成を利用することができない。
一方でProcessingには合成モードを指定する機能があるので、そのような点で困ることはない。
おわりに
この記事では、Common Lispでもジェネラティブアートをさくっと作ることができることを示した。
Common Lispでもsketchというライブラリを使えば、点や線を描画したり、それらにインタラクションしたりするプログラムを書くことができる。ただ、生成画像の保存ができないことや色のブレンドモードを指定できないことなど、問題もある
これらの機能が欲しいとなったときは、ClojureのQuilであるなり本家Processingであるなりを利用したほうがよいように思えた。あるいは、自分でProcessingライクなライブラリを実装するとか……。
Common Lispでyesコマンドを実装した
この記事 Haskell で yes コマンドを実装した に触発されて、yesコマンドをCommon Lispでモダンに書いてみました。
速度があまり速くなく、識者のご指摘をいただきたいところです。
2017/12/01追記 ご指摘いただけました! 後ろの方に追記しました。
コード
package-inferred-systemで定義されていてroswellスクリプトをもつ、こんな感じの構成です。トリビアル。
cl-yes$ tree . ├── README.md ├── cl-yes.asd ├── main.lisp └── roswell ├── yes └── yes.ros 1 directory, 5 files
速度測定
では、速度を測定します。
GNU Coreutilsのyes
リファレンスとして、ふつうのyes
の速度を測ります。
$ yes | pv -r > /dev/null [9.28GiB/s]
あらまー。10秒走らせたら100GBですって。はやいですねー。
yes.ros
Common Lispの処理系はこんな感じでございます。
$ ros config externals.clasp.version=5.0 setup.time=3718530120 sbcl-bin.version=1.4.1 default.lisp=sbcl-bin
文字列を出力しつづけるyes関数はこんな感じでございます。シンプル。
(defun yes (out &optional (s "y")) (loop (write-line s out)))
とりあえず実行してみると、
$ ./roswell/yes.ros | pv -r > /dev/null [1.86MiB/s]
ええ、遅い…。ビルドしてみるとどうなるでしょうか。
$ ./yes | pv -r > /dev/null [3.04MiB/s]
出だしの速度が改善されただけでした(ビルド前も5秒待つと、これくらいの数値)。なにが原因なのでしょう。試しにprinc
関数に書き換えてみます。
(defun yes (out &optional (s "y")) (loop (princ s out) (terpri out)))
$ ./yes | pv -r > /dev/null [4.62MiB/s]
ちょっとだけ速くなりましたが、なんでprinc
のほうが速いんだ。format
でも同じくらい。
(defun yes (out &optional (s "y")) (loop (format t "~a~%" s)))
$ ./yes | pv -r > /dev/null [3.59MiB/s]
ちなみに最適化宣言をしても、とくに結果は変わりませんでした。
(defun yes (out &optional (s "y")) (declare (optimize (speed 3) (debug 0) (safety 0))) (loop (format t "~a~%" s)))
$ ./yes | pv -r > /dev/null [3.04MiB/s]
というか、むしろちょっぴり遅くなった…?
そもそもGNU coreutilsのyesが謎の速さであって、そっちがおかしいのだろうか…。
ううむ、識者のご指摘がほしい…。
速い出力への道 (追記)
この記事を公開したところ、佐野さんから「バッファすると速いよ」とご指摘いただきました。そして併わせて、CFFIを通じてCの文字列でバッファする実装のコードが。こちらを手元の環境で実行すると、本物のyes並の速度が得られました…!
$ ./yes | pv -r > /dev/null [9.33GiB/s]
同じ内容を吐くだけなら一度だけバッファに溜めて、あとはそれをまとめて出力すればいいのです。目的に合わせて柔軟に思考しましょう。GNU Coreutilsの実装もまさにそのようになっています。
では、これをCommon Lispだけでやるとどうなるのか。ちょっと気になりますね。なのでやってみました。
(defun make-buffer (s &optional (times 10000)) (with-output-to-string (out) (dotimes (n times) (write-line s out)))) (defun yes (out &optional (s "y")) (let ((buf (make-buffer s))) (loop (princ buf out))))
$ ./yes.ros | pv -r > /dev/null [ 358MiB/s]
速度は、愚直な実装よりはずいぶん速くなりましたが、バッファのサイズに関わらずこの数字で頭打ちになります。GNU Coreutils yesの4%くらい…。Common Lispのストリームを挟むと遅いんでしょうかね。
まとめ (追加)
Common Lispにおいて猛烈な出力速度が求められる場合は、CFFIを使ってCの配列に出力内容を自前でバッファするのがベストプラクティスであるようです。
どうしてぼくはCommon Lispを書くのか
この記事は技術記事ではありません。自己分析のための独白のようなものです。
この記事を書いた経緯
雲が空高く流れる季節となったこのころ、二〇一七年十月末日をもって、ぼくは無職になりました。こうなると平日のほぼ全ての時間、独りで過ごすことになり、さまざまなことが頭をよぎります──人生のことや、うず高く積まれた積ん読のこと(読め)、懐具合のこと、身体のこと、年齢のこと……。
そんな中でもとくに頭を悩ませるのは、やっぱり仕事のことです。
次の職場でうまくやっていけるかしら──
ぼくは何を仕事としたいんだろう──
仕事とは──
そういう悩みを書き出すことで整理し納得や気付きなんかを得るために、この記事を書いています。
ここでは、なんでぼくはCommon Lispに固執しつづけ、時に他の言語をディスってしまうほどCommon Lispが大好きなのか、将来もCommon Lispをやりたいのか、を考えていきます。
まずは、なんでCommon Lispはじめたんだっけという回想からやってみます。
なんでCommon Lispを始めたか
中学生のころから、プログラミングをすることについて、興味津々でした。
きっかけは「自分でゲームをつくれるなんて楽しそう!」程度のものだったと思います。あるいは、鈴木光司の『ループ』を読んで、人工生命の分野に興味が芽生えたということもあったと思います。が、当時できたのはせいぜい、技術家庭の教科書にBASICが載っているのを見、家のパソコンに「N88互換BASIC」というソフトを導入して、ちんまりとした描画プログラムをつくって遊ぶことくらいでした。
まだその当時はLISPのLの字も知りませんでした。
また、Wikipediaを知り、いろいろな情報が簡単に手に入ることを学びます。コンピュータと人工生命への興味はとても高まっていましたから、それらについて記事から記事へと関連を辿って貪るように読んでいた記憶があります。その中で、確実にLISPについて触れたはずですが、まだLISPを特別に意識はしていませんでした。せいぜい「チューリング機械というものがあるのか、なんかよくわからんけどカッコイイ!」くらいのものでした。
そうしてC言語は読み書きできないまま、大学に入ります。
大学では、RoboCupに関するサークルに入り、AIBOを動かすC++のソースコードやGNU/Linuxに触れました。ちなみにC++は未だによくわかりません。
そこでアクチュエータの制御やカメラの画像認識、行動のタスクスケジューリングを行うコードに触れ、また講義で人工知能系の基礎を学ぶ中で、人工知能からは興味が離れていきました。当時のぼくにはどれもレベルが高すぎ、泥臭い数学が手に負えないという、強い実感があったのかもしれません。あるいは、大好きなSFに出てくる「強いAI」には程遠いと思ったからかも。
そんなぼくがLISPとファーストコンタクトするのは、大学の文化祭でした。
文化祭で、図書館が破棄する本の無料開放をやっていました。当時から本を所有することが大好きで、もちろんお金がなかったぼくは、タダで貰えるのならと並べられている古い本を物色していました。その中にあったのがこの本です。
当時、LISPはPascalやFORTRANなどと同じくいわゆる古代の言語であり、コンピュータの歴史の中で重要な役割を演じてきた言語である、と認識していました。歴史的に重要な言語であるなら、学ぶ価値があるだろう、というふうに考えたわけです。(その考え方でCOBOLに手を出さなくてよかった!)
この『これがLISPだ!』、LISPの基礎からエキスパートシステムの構築までを解説しています。いま思うとちょうどいい本なのですが、マクロの応用あたりからだんだん理解が難しくなっていったのを覚えており、読破はできませんでした。処理系で動かさず、紙の上だけで読んでいたからかもしれません。
とはいえ、ここでLISPの基礎概念に慣れ親しみ、Common Lispの世界に突っ込む準備はできていたわけです😏
そして大学院へ入ります。
大学と大学院が違いあまり知り合いがいなかったため、講義の合間は図書館で本を物色していました。コンパイラのドラゴンブックとかラムダ計算の本とかパラパラと眺めていた覚えがあります。同じ区域のコンピュータ関連書籍を眺めていると、そこにはこの本が。
LISPの本自体はそこに何冊かあったのですが、この本の目次を見て驚きました。他の本はエキスパートシステム等現実感のないものを終わりのほうで書くのですが、音楽配信サーバの作成は本当に「実用」でした。
で、始めのほうを読んでみるわけです。
1億マイルも離れた100万ドルもするハードウェアで走っている動作中のプログラムのデバッグは非常に面白い経験だった。問題の発見と修正には、宇宙船で走っていたread-eval-printループがなくてはならない貴重なものだった。
当時、LISPを始めるにあたってSchemeかCommon Lispか、どちらにしようか迷っていました。が、この文句と、バイナリファイルをパースする章を読んで、Common Lispを始める決意を固めたのを覚えています。
はじめた決め手は『実践Common Lisp』に違いありません。
しかしそれを手に取ったのは、『これがLISPだ!』を読んでいてLISPにちっぽけながら知見があったからだと思っています。そして、人工知能・人工生命に興味を持っていたから、LISPについて知ろうと考えました。
あのころ土日の昼間を外にも行かずWikipediaばかり眺めて過ごした日々よ、ありがとう。
どうしてCommon Lispなのか
とはいえ、その遍歴の中でさまざまな言語の名を聞き、実際に触れてみたりもしています。JavaとかPythonとかHaskellとかJavaScriptとかRubyとか。でもなんで未だにCommon Lispを使い続けているのでしょう。
ぼく自身ふしぎなので、ぼくがCommon Lispを愛して止まない理由について、考えてみようと思います。
規格で仕様が定められている
ご存知のとおり、Common LispはANSIで仕様が定められています。このことが意味するのは、どんな処理系のどんなバージョンでも、Common Lispを名乗る限りにおいて一定の動作をすることが保証されていることです。
つまり、言語仕様が進化を遂げていき、過去に書いたコードが(ライブラリバージョンのせいではなく)言語仕様の意味で不正なコードになって動かなくなるということがない、ということです。
たとえば、Common Lispにおける実験的な遅延シーケンス(ジェネレータなどが提案された)のライブラリcl-series は2000年ごろに書かれたライブラリのようですが、いまでもばっちり動きます。同じく規格のあるC言語でも古いけど現役のコードがありそうですが、RubyやPythonで同じようにいくでしょうか?
過去の資産がいつまでも有効であることは、Common Lispの大きな魅力です。
構文について覚えることが少ない
ご存じのとおり、Common Lispは──というかLISPは──S式でコードを記述します。LISPの構文について覚えておくべきことは少しだけで、それは括弧に始まり括弧に終わり、先頭にやりたいことを書く、ただそれだけです。
Common Lispでは、LISP特有の伝統的マクロによって新たな構文が導入されるとき、新しい構文要素についていちいち気を払う必要はありません。新しい構文要素に目を慣らすことなく、その構文が何をするのかにだけ気を向けることができます。他人のコードを読んでいるときにかつて出会ったことのない構文を見て(たとえば$
などの半角記号なので)調べかたに困る、というようなことがありません。
もちろん、リーダマクロが導入されたときは例外です。
動的に環境にアクセスできるため開発がしやすい
ご存じのとおり、Common Lispでは実行中のプログラムが参照する関数の定義を、実行時に差し替えることが可能です。たとえば、実行中のグラフィックスプログラムが参照している描画関数の定義を変更し、REPLから流し込んでやることで、実行中のプログラムの描画内容を即座に変更する、ということが可能です。また、GNU Emacs用のプラグイン、SLIMEによってREPLの機能が大幅に増強され、開いているソースコードに書かれた関数の定義をCommon Lispのプロセスに送る、ということもできます。
そのためプログラムを書くとき、まずは一番小さな部品をREPLで組み立て、次に別の小さな部品をREPLで組み立て、ということを繰り返していき、最後に組み上げるというボトムアッププログラミングの方法でつくり上げていくことができます。トップダウン的にコードを書いていくと、どうしても初めて動かしたときにてんこ盛りのバグへ対処せねばならず、気持ちを保つのにぼくは苦労します。
単に対話的につくり上げていくスタイルのほうがぼく自身に合っているだけかもしれませんが、それを究極に推し進めたCommon Lispは、そんなぼくにとって最高の道具です。
将来もCommon Lispをやっていきます
好きな点が思ったよりもたくさん出てきました。
新しい風を感じたくて、あるいは知らない概念を学びとるために、別な言語を学び始めることは何度かありましたし、これからもあると思いますが、ぼくは上記の理由からいつかCommon Lispに戻ってきてしまうのだろうなあ、というのが整理してみての実感です。もしぼくが別な言語に完全に移行するときがくるとしたら、その言語はきっとCommon Lispを遥かに越えた言語であるに違いなく、それはそれでいいのかもしれません。
なので/ですが、今ぼくはCommon Lispが一番好きです。
Oji --- バイト列の文字境界を識別する
Ojiは文字符号化の方法が分かっているバイト列における「一文字」の範囲を識別するライブラリです。
どうしよう、これ。
いちおう動機
flexi-streamsにASCII範囲外の文字を渡してstream-read-char
すると、正しく一文字を認識してくれない現象を発見しました。が、これは誤りでした。
以下がまず、CL標準のread
でテキストストリームから一文字を取ってきた場合です。ひらがなの「こ」が取得されているのがわかります。
;; 文字列の一文字目「こ」が読まれる CL-USER> (defparameter str "これはペンです") STR CL-USER> (with-input-from-string (in str) (format t "read from stream: ~s~%" (read-char in))) read from stream: #\HIRAGANA_LETTER_KO NIL
次に、flexi-streamsのほうでstream-read-char
した場合です。こちらは、flexi-streamsのin-memory stream (バイナリストリーム) をbivalentなストリーム (バイナリ・テキスト両ストリームとして扱える) に変換した場合に発生します。
;; 文字列の一文字目「こ」ではない文字が読まれる CL-USER> (defparameter octets (flex:string-to-octets str :external-format :utf-8)) OCTETS CL-USER> (flex:with-input-from-sequence (in octets) (format t "read from flexi stream: ~s~%" (trivial-gray-streams:stream-read-char (flex:make-flexi-stream in)))) read from flexi stream: #\LATIN_SMALL_LETTER_A_WITH_TILDE
flexi streamに対してのstream-read-char
では~
が読み取られていますが、これは「こんにちは」をUTF-8で符号化したときの最初のバイトの値です。
;; UTF-8の文字としてではなく、ASCIIバイト列として一番目を見てみる CL-USER> (format t "first byte as a character: ~s~%" (code-char (aref octets 0))) first byte as a character: #\LATIN_SMALL_LETTER_A_WITH_TILDE NIL
と、ここまで書いてて気づいたんですが、実際には、flexi streamを作るときにexternal-format
を指定すれば、ちゃんとデコードしてくれます。ほんといま気づいた……😢
;; なんとexternal-format指定漏れ! CL-USER> (flex:with-input-from-sequence (in octets) (format t "read from flexi stream: ~s~%" (trivial-gray-streams:stream-read-char (flex:make-flexi-stream in :external-format :utf-8)))) read from flexi stream: #\HIRAGANA_LETTER_KO
flexi-streamsは古くからあるライブラリだし、そういう部分は枯れてて当然ですね。
はずかしい……。消えたい……。
使い方
CL-USER> (setf moji (oji:load-bytes (babel:string-to-octets "これはペンです" :encoding :utf-8) :utf-8)) CL-USER> (oji:encoding moji) :utf-8 ;; read-charは未実装 CL-USER> (oji:read-char moji) #\HIRAGANA_LETTER_KO ;; it's not #\LATIN_SMALL_LETTER_A_WITH_TILDE CL-USER> (oji:boundary moji) ((0 . 2) (3 . 5) (6 . 8) (9 . 11) ...)
感想とか今後とか
どうしよう、これ。
当初の目的としてはふたつあって
- flexi-streamsに妙な点があるので直したい (そんな点なかった)
- バイト列から文字境界認識や区点番号へのデコードをすることで、文字符号化方式への理解を深める
- そこからinquisitorのエンコーディング判定部分の改善に繋げたい
だったんですが、そのうち1が潰えてしまい、えええーーー……😩
そうすると、このライブラリ、何に使えるのだろう。とりあえずUTF-8の文字境界認識と区点番号デコードができたので、るんるん気分で「これからinquisitorサポートの文字コードについて実装していきます!!」とか書こうと思ってたのに……。
動揺していますが、まあ遊びで使うことはできそうだし、調べるの楽しかったので、開発がんばろうと思います!