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サポートの文字コードについて実装していきます!!」とか書こうと思ってたのに……。
動揺していますが、まあ遊びで使うことはできそうだし、調べるの楽しかったので、開発がんばろうと思います!
Lisp GNU Lesser General Public Licenseを和訳してみた
LLGPLを和訳してみた
T/Oなんですが、それだけでは寂しいので経緯と概要を記しておこうと思います。
LLGPLとは
Lisp (特にCommon Lispか)の事情に対応するための前文を付け足したLGPLです。LGPLについては概要、経緯ともにWikipediaの当該記事が詳しいです。
Common Lispはイメージ指向の言語であり、ひとつの大きなイメージの中に標準関数群、ライブラリ(パッケージ)を全て読み込んでコードを実行するような形態を取っています。LGPLにおいて、以下に引用するのように、静的リンク・動的リンクを区別する文言がありますが、
『ライブラリ』のいかなる部分の派生物も含まないが、それとコンパイルされ るかリンクされることにより『ライブラリ』と共に動作するようデザインされ ているプログラムは、「『ライブラリ』を利用する著作物」と呼ばれる。その ような著作物は、単体では『ライブラリ』の派生著作物ではないので、この契 約書の範囲外に置かれる。
しかし、「『ライブラリ』を利用する著作物」に『ライブラリ』をリンクして 実行形式を作成すると、それは「『ライブラリ』を利用する著作物」ではなく、 『ライブラリ』の派生物となる(なぜならそれは『ライブラリ』の一部を含ん でいるから)。そこで、実行形式はこのライセンスで保護される。
--- GNU 劣等一般公衆利用許諾契約書, http://www.opensource.jp/lesser/lgpl.ja.html
Common Lispでは、プログラムの目的のためにLGPLでライセンスされたライブラリを利用すると、実行時にプログラムがランタイム(イメージ)上にロードされます。そのため、その状態でCommon Lispのランタイムは上記LGPLの条文「『ライブラリ』を利用する著作物」に『ライブラリ』をリンクして 実行形式を作成すると
に該当してしまい、ランタイムがライブラリの派生物となってしまいます。
2017/9/12 16:52訂正
ランタイムはライブラリの派生物にはなりません。ご指摘のとおりなのですが、実行可能形式の作成時にリンクすると、ライブラリの派生物になります。
訂正ここまで
例として、ゲームなどのアプリケーションが挙げられます。
LGPLでライセンスされたあるゲームエンジンを利用するゲームアプリケーションがあったとすると、そのゲームは直感的には『ライブラリ』を利用する著作物
ですが、LGPLの規定によってゲームエンジンの派生物となります。
これはLGPLが、動的リンクが可能であるC言語で作成されたプログラムを前提として作成されているために発生する問題です(っていうかWikipediaのLGPL記事の「プログラミング言語による特異性」にこのこと書いてあった……)。
そこで、Common Lispのイメージ指向な事情に対応した条項を追加する必要がある、ということで作成されたのがLisp GNU Lesser General Public Licenseです。
ちなみにLLGPLは、LGPL v2.1を元にして作られたライセンスです。
翻訳するに至った経緯
このごろ(やっと)オープンソースプロジェクトのライセンスを気にするようになりました。
RMSやFSFの主張する「自由なソフトウェア」という考えかたに共感を覚え、それを明示するライセンスの内容や抱える問題、ソフトウェアとの関係に興味をもったからです。そして、自分が書いたコードはどのライセンスであるべきなのか、など。
んでも、ライセンスって原文が英語なので、読むのが苦しい。非常に苦しい。
IPAが良いドキュメントを公開してましたよ。https://t.co/O4B6yxrA37
— TANIGUCHI (@ta2gch) 2017年9月3日
そこで谷口さんにこのドキュメントを教えてもらいました。
ふう。これでLGPLまではよしとします。
ところでLispの上述の事情を考慮したLLGPLなるものがあるようで、存在は以前から知っていましたが、英語なので辞書を引き引き苦労してなんとか読むのですが、これを毎回やるのがなかなかつらい。くるしい。
どうやらLGPLと違いマイナーなせいか、和訳はないようでした。
ならば自分で和訳すれば、理解も深まるしいいのではないか。
……というのが、翻訳に至った経緯です。
LLGPL.ja
というわけでひととおり翻訳しました。
リポジトリの目的は理解を助けるためであり、厳密さが要求されるときにはちゃんと原文読んでね! というスタンスでいこうと思います。
そういえば、和訳自体のライセンス、どうすればいいんだろう。GNUのドキュメント用ライセンスがあったと思うけど……。
2017/9/11 の夜に追記
以下のご指摘を頂き、上記のライセンスどうしようの部分は不適切だと思われたため、削除します。翻訳者はぼくですが(リポジトリに明記していない)、原著作者はFranz Inc.であり許諾はないため、ぼくにライセンスを検討する理由はないことが理由です。
著作物のライセンスは、(原)著作者や権利者だけが決めることができます。派生物とみなされる翻訳について、preambleのCopyright/翻訳者が誰であるか/何が対象なのかが明示されず、許諾があったように見えないのに、ライセンスの適用を検討されている点には違和感を覚えました。 https://t.co/Cg58OGhTXV
— heno (@_heno) 2017年9月11日
これはぼくの不勉強のせいでした。ご指摘いただいたこと、お礼申し上げます。
2017/9/12 01:01 追記
そもそもFranz Inc.にコピーライトがあるのに、勝手に翻訳を公開していいのか怪しいため、一旦同社にお伺いのメールを送りました。返答次第では翻訳をWeb上から削除します。
2017/10/04 19:04 追記
Franz Inc. にメールにて確認し、http://opensource.franz.com/preamble.html にリンクを張ってあれば翻訳を公開してよい
旨の返答を頂きました。
One --- 手短に入出力を扱うフレームワーク
One — 手短に入出力を扱うフレームワーク
Oneはファイルや標準入力に対する操作を、bashのパイプ処理のような感じで手短に記述するためのライブラリ・フレームワークです。
イチの逆襲(あらまし)
時は宇宙世紀2X17年。ぼくはまだ、CSVの処理をするのにシェルコマンドを使っていた。
$ cat nums.csv name1,1 name2,3 name3,5 $ cat nums.csv | awk -F , '{sum=+$2}END{print sum}' 9 $
シェルコマンドとパイプラインは便利だ。GNU coreutilsとsedとawkがあれば、宇宙海賊も倒せそうな気がする。
しかし待て。
Common Lisp使いたるこのぼくが、シェルコマンドに甘んじていてよいものか。そもそもsedやawkはそれ自体が独立した言語になっていて、いちいちmanを引かねばならないくらい複雑だ。
ならば、とりあえず合計する部分だけでも、Common Lispでやってみよう。
$ cut -d ',' -f 2 nums.csv | ros run -e '(print (loop for line = (read *standard-input* nil :eof) until (eq :eof line) sum line))' -q 9 $
おいおい、これはなんだ? この伸びたヌードルみたいなコードは!?
整理しよう。問題は二つある。
ひとつは、標準入力を表すだけの変数の名前が*standard-input*
と、長すぎる。
もうひとつは、ファイル、ストリームなどに対する処理が抽象化されていないため、長ったらしいループをすべて書いてやらければならないこと。
つまり、こういうライブラリを作れば、問題は解決するんじゃあないか:
- 標準出力に短いタイプ数でアクセスできる
- ファイル、ストリーム(と、あとシーケンス)に対する繰り返し処理を抽象化する
- 複数の処理を、パイプのように(あるいは関数合成のように)連結できる
──そして、それをぼくは書いた。
前のシェルコマンドの例はこのようになる。
$ cut -d ',' -f 2 nums.csv | ros run -s one -e '(one:for* - < one:read* +> + 0)' -q 9
なんてこった! あのヌードルみたいなクソが見る影もねぇや!!
こうして、入出力を気軽に、手短に扱えるようになったぼくは、これをつかってログファイルの集計、データの加工をperlやrubyやpython(めんどくさいことをやらせるにはちょっと長いような気がする)を覚えることもなく、手に馴染んだCommon Lispでやってみようと、銀河の荒野へと歩き出したのだった。
Common Lispがいつか宇宙を照らす光になると信じて……。
っていう。
これは何ぞのものか
以前つくったこのライブラリ
をより良くしたものです。以前のバージョンは繰り返し処理や読み込み関数が決め打ちになっていましたが、もうちょっと柔軟なしくみにしました。Common Lispでシェル芸をやりたかったため、以下のものを参考にしたフィーリングになっています。
- シェルのパイプライン
- 数学の関数合成 (Haskellの
$
) - Common Lispのseries (関連記事)
sed/awk等の独自言語つらい
と言ったものの、これもわりと独自言語になってしまいました。なので「フレームワーク」と大仰ですが呼称しました。
ちなみに、あらましの例を全部Common Lispでやると、パッケージ名などの影響で、どれよりも長くなります。かなしみ。
$ ros run -s one -s split-sequence -e '(one:for* #P"nums.csv" < one:read-line* $ (split-sequence:split-sequence #\, _) $ (nth 1 _) $ read-from-string +> + 0)' -q 9 $
使用方法
インストール
roswellを使って簡単に導入できます。
$ ros install t-sin/one
ワンライナーとして実行するなら以下のテンプレートが有用です。
$ ros run -s one -e '(one:for ...)' -q
REPLで叩くならquicklisp(roswellの導入時にインストールされている)を使って
> (ql:quickload :one)
で導入できます。
基本構文
one:for
マクロを使います。また、入出力にはone/io
パッケージの関数を使うとよいです。もし、とりあえず値を出力したかったらone:for*
マクロを使うのも手です。
基本的な形は以下です:
# ここに出てくる`<>`や`[]`、`*`は説明用の記号 > (one:for <input> [<connective> <operation>]*)
<connective>
(結合子とよぶことにします)は、シェルでいうところのパイプ、処理(たとえば、末尾に文字!
を追加する、など)を繋ぐものです。<operation>
の部分に処理がきます。<operation>
の結果を左から右に<connective>
で繋いでいく、というのが基本的な考え方です。
結合子のふるまい
結合子には五種類あり、それは$
と<
と>
と+>
と?
です。+>
だけ二文字です。それぞれ次のようなふるまいを表します。
$
: 処理の合成
処理を合成します。具体的には$
の直前までの処理に、$
の直後の処理を合成します。例として、文字列にa
を追加する処理、b
を追加する処理の合成を示します。
> (one:for "もじれつ" $ (format nil "~aa" _) $ (format nil "~ab" _) $ one:print*) もじれつab
ここで初出の_
は、直前までの処理の結果を表すプレースホルダーです。詳細は後述します。
<
: pathname、ストリーム、シーケンス上の走査
pathnameやストリーム、シーケンスの各要素について、後続の処理を適用します。各要素は<
へのパラメータで指定します。例として、典型的なcatコマンドの動作を示します。
> (one:for #P"text.txt" < one:read-line* $ print) "一行目" "二行目" ...
ここではread-line
関数を使ってファイルを走査しています。なお、ストリームにはテキストストリームを仮定し、EOF時に:eof
を返すことを想定します。one:read-line*
は、そのように改造したread-line
です。
>
: 処理結果の蓄積、操作
直前までの処理の結果を内部に蓄積し、リストにして次の処理へ渡します。次の処理に渡すときの変換方法をパラメータで指定します。例として、文字列の各文字をリストに溜めて、印字します。
> (one:for "のび太さん!" < one:read-char* > identity $ one:print*) (の び 太 さ ん !)
上記では溜めたあと何もしていませんが、たとえばソートすることができます。
> (one:for "のび太さん!" < one:read-char* > (sort _ #'char<) $ one:print*) (さ の び ん 太 !)
_人人人人人人人人人人人人人_ > さ の び ん 太 ! <  ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄
ただしこの蓄積処理は全入力をメモリに読み込むため、ソートするときぐらいにしてください。商用環境でwebサーバのログをソートするのに使ったらダメだぜ。
+>
: 処理結果の畳み込み
こちらはメモリに全データを残しておく必要がないときに使います。いわゆるreduce
、Haskellでいうfold
的なふるまいをします。例は、文字列のリストを結合するなど。
> (one:for '("のび太さん" "のび太さ" "の" "のびたさぁーん!") < cdr +> (lambda (x y) (format nil "~a~a " x y)) "") "のび太さん のび太さ の のびたさぁーん! "
reduce処理なので、畳み込み処理の後ろに初期値を指定できます。nilでよい場合は省略できます。
?
: 処理結果の選別
処理結果を述語でフィルタします。
(one:for '("のび太" "スネ夫" "ドラえもん" "野比のび太") < cdr ? (search "のび太" _) $ print) "のび太" "野比のび太"
簡易ラムダ式
さて、ここまでで何度か出てきていますが、関数名に#'
を付けなくてよかったり、(search "のび太" _)
など、いくつか省略記法を用意しています。
一つは、パラメータとして単にシンボルが現われたとき、#'
を自動で付与するものです。
もう一つは、簡単なラムダ式です。one:for
マクロの中で(search "のび太" _)
などと書くと、(lambda (input) (search "のび太" input))
などと展開されます。
これで、少しは短かく記述ができるはず……。
2017/8/30追記
リーダマクロ#/
に機能を分離してみた。ただし、oneをロードするだけで有効になってしまう。だれか、for
マクロの中でだけリーダマクロを有効にする方法をおしえてください……。
以上がOneの説明です。
Oneが抱えている問題とこれから
Oneのテストをひととおり書き終えたため、勢いで書き散らしていますが、現在以下のような問題が残っています。
記号
$
,?
,<
,>
,+>
と五つの記号を使っていますが、この記号でいいのかどうか…。パイプっぽくするならリーダマクロを局所的に有効にするという手もあるし、|
始まりに統一することは可能。あるいは、基本の関数合成に寄せて、$
始まりにしてもよいかもしれない。
記号は、変更が容易なのであまり気にしなくてもよいかもしれない点です。
リスト以外のシーケンスでscanのパラメータを無視している
リスト以外のシーケンスについて、scanのパラメータ(input < next-fn
のnext-fn
)が無視され、cdr
再帰のような繰り返し処理しかできません。
リストについてはloop
のby
で実装していますが、ユースケースを想像できなかったので保留中の問題です。
入出力関係の補助関数があってもよい?
scan時の読み込みやとりあえずprintするときなど、いちいち(lambda (stream) (read-line stream nil :eof))
などと入力したくないため、one:read-line*
などの簡易関数を用意しています。しかしながら、それでも長いですよね。read-line
ならrl
とすごく縮めるとか、パッケージのニックネームに一文字パッケージ(例: o:for
)を復活させるとか、したほうがよさそう。
split-sequence:split-sequence
関数のフルネームの長さには、涙を禁じえない……。
ドキュメント皆無
今の時点では、この記事のみがドキュメントです。さっきやっとテスト書いたのでしゃーない。なので、READMEを書かなければなりません。
2017/8/30追記
とりあえずREADMEを書きました。
SBCLでテスト落つる
テスト落ちます。手元でも確認済みですが、なにも表示してくれないので、これから調査します。
Travis CI - Test and Deploy Your Code with Confidence
2017/8/30追記
深町さん作のユニットテストフレームワークroveを使っているのですが、そちらにある問題のようでした。記事を見ていただいたようで、すぐに修正していただけたため、こちらの問題は解決済みです。ありがとうございました。
意見を聞きたい
是非や好みがあるとは思いますが、そこそこのグッジョブだと思っています。なので、作者以外のご意見を伺いたいところです。