重い処理をSLIMEからCommonLispをやらせていてちょっと感動した。
なんとEmacsが使えるのだ1。
elispにやらせていたときはなーんもできんようになっていたのに。処理自体も全然、速いし。これからはelispではなく、CommonLispを使うようになるかも。
Footnotes:
よくよく考えたら当たり前
とりあえず、データを加工してごっそり変換するプログラムをつくった。
DBに格納しておいてそれをあとで利用しようというわけ。
ところが変換中、元々残りわずかだったMacBookAirのディスクスペースが満杯になってしまった。あわてて中断したけれど、いかんじゃないか、これは。
まいった。
あれだな、これはPostgreSQLを別のパソコンに持っていくのが吉か。
そう思って全然、使っていないBeelink S1のUbuntuにPostgreSQLをインストールした。psqlも。基本になるデータをpg_dumpでつっこんでリモートアクセスできるように設定して。
動かした。
MacBookAirのCommonLispから。20年分近いデータだ。時間がかかるだろうなぁ、と思っていたらBeelink S1が固まった——固まったと思った。terminalが反応しなくなったのだ。
これだから安物は。
熱暴走か——Beelink S1は熱が溜りやすいとか、なんとかいう噂を耳にしていたのだ。たしかに筐体は熱くなっていた。けれど、何度か、試してみているうちに、原因はBeelink S1ではないことに気づいた。
topコマンドでみると、「kswapd0」というプロセスで固まっている——ように見える。「kswapd0」とはなんぞ。ググってみてページングプロセスらしいことがわかる。そうやってtopコマンドの出力を見直してみると、たしかにswapメモリが0になっていた。Linuxのせいかよ!
いやこんなに簡単に固まるようじゃ、世間様が使うわけはない1。
固まっているように見えるが、実は非常にレスポンスが悪くなっているだけで——何もできないけど。もしかしたら自分のせいじゃね?
組んだプログラムが悪いんじゃね?
でも。
毎回、commitするようにしても固まるし。
毎回、connectしなおすようにしたらsocketを取得できなくなってCommonLisp側がハングアップしてしまうし。
あかんじゃないか。
そりゃあ、プログラムを動かさなければ、固まらないさ。それじゃ、意味がない。swapを拡張しても1年分ほどで固まっているから問題の解決にはならんだろうし。
たぶん状況から考えてリモート接続していると、PostgreSQLがメモリを食い潰しているのだろう(ローカルで動かしたpg_dumpはうまくいっているのだから)。そこでUbuntuにCommonLispの環境をつくってローカル接続からデータ変換するようにしたところ。
うまくいった。
swapメモリはまったく消費せずに2。
Lispでプログラミングしていると、粘土をこねているような感覚がある。
ぐにゅぐにゅと、つくったものをくっつけたり、ちぎったりしているような感覚。ほかの言語ではそんなことはないのでおそらく、Lisp系の言語に固有な感じなのだろう——不思議なことにElispよりCommonLispの方がその感覚が強い。CommonLisp特有の機能を使っているわけじゃないのに。気持ちの問題かな?
ほかのプログラム言語はもうすこし不自由な感じで——こねられない(感覚としていは切り貼りしているような感じ。粘土より硬い)——、何でだろう、とずっと疑問だった。
もしかしたらそれはS式のおかげではないか?
Lispが嫌われる大きな理由のひとつ。あのカッコだらけの姿。
はじめてLispのプログラムを見たときはのけぞったあのカッコ。こんな言語、わけがわからん、と思ったのだけれど。
これはぼくだけのことなのかもしれないけど、プログラムを組むとき、トップダウンに上から単純に組んでいくわけじゃない。かといってボトムアップともかぎらず、いきなり真ん中ということも多い。
これは形が見えているところから組むからだろう。——重要なのは組んでみると、まちがいに気づくことが多々あるということだ。
この形はよろしくない。
で、プログラムをこねる。
よくやるのがロジックを外出しすることだ。同じロジックを組みはじめると、イラッとする。それで共通関数にくくりだす。
これがLispだと楽にできる。
だってすでにカッコでくくられているから。Ctrl-Meta-kでkillしてyankするだけ。ほかの言語だと、行レベルでの範囲選択になるので範囲指定に頭を使うし、ちょっとほかの部分とまじっていたりしてひと手間かかる。もちろんLispの場合でもロジックがまじってしまっていることはあるけど、概ねだいじょうぶ。たぶんロジックがカッコでくくられているからだろう。まじっていない部分のみを取り出すのは容易だ。
逆に、これは細かくしすぎた、と思ったときはカッコごと、ほかのカッコの中に移動させて、まぜる。
こうやって考えると、LispがS式であることは重要。
hyperspecのデータをどこぞからもってきてローカルの保存して「common-lisp-hyperspec-root」に場所を定義すれば、よいらしい。
common-lisp-hyperspec-root is a variable defined in ‘hyperspec.el’. Its value is "http://www.lispworks.com/reference/HyperSpec/" Documentation: The root of the Common Lisp HyperSpec URL. If you copy the HyperSpec to your local system, set this variable to something like "file://usr/local/doc/HyperSpec/".
となっていたのだけれど、ewwだと「file://usr/local/doc/HyperSpec/」にしていると、うまくいかなかった。ftpで接続へ行ってしまう。
なので色々、試した結果。
"file:/usr/local/doc/HyperSpec/"
「//」を「/」にした。
ソースを見ると、cl-dbiはdbiというpackageだった。
(defpackage dbi (:use :cl :dbi.error) (:nicknames :cl-dbi) (:import-from :dbi.driver :list-all-drivers
CL-USER> (cl-dbi::select "123") ; in: DBI::SELECT "123" ; (DBI::SELECT "123") ; ; caught STYLE-WARNING: ; undefined function: DBI::SELECT ; ; compilation unit finished ; Undefined function: ; DBI::SELECT ; caught 1 STYLE-WARNING condition ; Evaluation aborted on #<UNDEFINED-FUNCTION SELECT {10065F6893}>.
それに気づかずに、自分もdbiというpackageにしたら。
CL-USER> (defpackage dbi (:use :cl :cl-dbi) (:nicknames :keiba-dbi)) (in-package :keiba-dbi) (defun select(sql) sql) WARNING: DBI also uses the following packages: (DBI.ERROR) 【割愛】 The ANSI Standard, Macro DEFPACKAGE The SBCL Manual, Variable *ON-PACKAGE-VARIANCE* SELECT
cl-cbiにselectが定義されてしまった。
DBI> (cl-dbi::select "123") "123" DBI>
うわっ。
これってcl-dbiを拡張してしまったってこと?1
関数とか、conflictしたらどうするのよ、CommonLispって変、と一瞬、思ったのだけど2、よくよく考えたらC言語とかでも関数名がかぶったら片方が有効になるだけだった。
そう思うと、javaの、あのディレクトリ構成にマップされた命名規則はconflictを避けるためのものなんだなぁ、といまさらながら。3。
sql文を組み立てるため、formatを使った。
CommonLispのformatは全然、ちがうというのは知っていたのだけれど、まぁ、「~S」が文字列だろう、と簡単に考えたら全然、ちがっていた。
CL-USER> (format t "あああ~Sかかか" "ABC") あああ"ABC"かかか NIL
まさか「"」付きになるとは。
CL-USER> (format t "あああ~Aかかか" "ABC") あああABCかかか NIL
「~A」か。
しかも第一引数で出力先を指定している。nilでは標準出力になる。tで変数へ出力できる。まるでprintf()とsprintf()みたいだ。でもformatの構文は全然、ちがう。
cl-dbiで返ってきたデータにはめんくらってしまったのだけど。
こんなデータ。
(:|date| 3281472000 :|place| "A7")
DBのカラム名と値が交互にならんでいる。
きっと何か、うまくやる方法があるんだとは思っていた。値をひっぱりだすのにgetfを使うというのは了解した。
CL-USER> (setq wrk '(:|date| 3281472000 :|place| "A7")) (:|date| 3281472000 :|place| "A7") CL-USER> wrk (:|date| 3281472000 :|place| "A7") CL-USER> (getf wrk :|date|) 3281472000
きっとkeyとしても使えるにちがいない。それがよくわからなかった。applyかな、とは思っていたのだけど、うまく動かない。
CL-USER> (defclass <wrk> () ((date :initarg :date) (place :initarg :place))) #<STANDARD-CLASS COMMON-LISP-USER::<WRK>> CL-USER> (apply #'make-instance '<wrk> wrk) ; Evaluation aborted on #<SB-PCL::INITARG-ERROR {10048A9723}>. CL-USER>
ふと気づく。
CL-USER> (defclass <wrk> () ((date :initarg :|date|) (place :initarg :|place|))) #<STANDARD-CLASS COMMON-LISP-USER::<WRK>> CL-USER> (apply #'make-instance '<wrk> wrk) #<<WRK> {1004641BE3}> CL-USER>
そっか。
defclassの定義で「:date」としていたのが、まちがいだったのだ。
CommonLispのsymbol名は基本、大文字小文字の区別がないのだけれど、「|」付の場合は区別しているという意味だった(それは一応は知っていた)。「|」付で定義すると、区別あり、と定義できるのだった(これには気づいてなかった)。DBのカラム名は普通、大文字小文字の区別があるのでこうなっているのだろう。
これでcl-dbiからの返却値を一気にクラスに流しこむことができる。
この「apply」を使うやり方に気づいてから一気にコード量が減った。ちまちまと値を設定しているところがごっそりなくなったので。
applyって凄い1。
それにしても「:」付のsymbolってどうやってつくるんだろう?
単純にinternしてもだめなんだよなぁ。
CL-USER> (intern "date") |date| :INTERNAL CL-USER>
いや、listが凄いのかも。
データのかたまりはやはりstructか、classにした方がいいよな。
EIEIOを使ったのときはクラスを定義した。
あれが丸々、使えるんじゃね?
そう思ってコピったところ、エラーになる。しかもdefclassのところ。何でよ。同じじゃないの。目を皿にしてようやく気づいた。EIEIOでは「:docment」だけど、CLOSでは「:documentation」だ……。
「~/.roswell/」の下をつらつら見ていると、「local-projects」というフォルダがあった。二カ所。
~/.roswell/local-projects/
~/.roswell/lisp/quicklisp/local-projects/
ああ、ここの下に自分がつくりたいプログラムをいれておけば、いいんだな、と見当をつけてとりあえず、「cl-project」でprojectを作成した。
CL-USER> (cl-project:make-project #p"~/.roswell/local-projects/keiba/" :author 名前 :email メールアドレス :license ライセンス :depends-on '(:cl-dbi :cl-ppcre)) writing ~/.roswell/local-projects/keiba/keiba.asd writing ~/.roswell/local-projects/keiba/keiba-test.asd writing ~/.roswell/local-projects/keiba/README.org writing ~/.roswell/local-projects/keiba/README.markdown writing ~/.roswell/local-projects/keiba/.gitignore writing ~/.roswell/local-projects/keiba/src/keiba.lisp writing ~/.roswell/local-projects/keiba/t/keiba.lisp T
(asdf:load-system :keiba)でsystemをロードすると、cl-dbiもロードされる模様。ふむ。なるほど。
CL-USER> (asdf:load-system :keiba) ; compiling file "/Users/yamada/.roswell/local-projects/keiba/src/keiba.lisp" (written 10 JUN 2018 09:29:31 AM): ; compiling (IN-PACKAGE :CL-USER) ; compiling (DEFPACKAGE KEIBA ...) ; compiling (IN-PACKAGE :KEIBA) ; /Users/yamada/.cache/common-lisp/sbcl-1.3.11-macosx-x64/Users/yamada/.roswell/local-projects/keiba/src/keiba-TMP.fasl written ; compilation finished in 0:00:00.005 T
(ros::load-system "keiba")でも同じみたいだ。
わからないのは
CL-USER> asdf:*central-registry* (#P"/Users/yamada/.roswell/lisp/quicklisp/quicklisp/")
なのに、#p"~/.roswell/local-projects/keiba/"をasdfが認識していること。qlが下記のようになっているのでRoswellはわからなくもないけど。
CL-USER> ql:*local-project-directories* (#P"/Users/yamada/.roswell/local-projects/" #P"/Users/yamada/.roswell/lisp/quicklisp/local-projects/")
不思議。
当たり前の話だけれど、REPLを立ち上げなおしたら1今までの作業がきれいさっぱり消えてしまった。あらためてちまちまとloadしなおさなければ、いけないとは思えず、たとえば、Emacsでいえば、load-pathみたいな仕組みがどこかにあるんじゃなかろうか?
どうやらASDFとか、quicklispとかがその仕組みらしいのだけれど。
require, ASDF, quicklispを正しく使う
上記以外にもあちらこちらのサイトを読んで考えこんでしまった。なんか、情報が錯綜している。頭の中がごちゃごちゃになってしまった。defsystemってなんぞ2。そこで「SBCL」とか、「asdf」のinfoを読みなおしながら自分の環境のパス構成を確認。なんか、おかしい。
「~/.sbclrc」とか、「~/common-lisp/」とか、見当たらない。
それを作成して設定すれば、いいのか? でもなんか、根本で道を誤っているような気がする。
あっ。
思い出した。
下記のサイトを見てRoswellを使ってsetupしたんだった。
Common Lispとリアル・ワールドを繋ぐ「Roswell」の紹介
ということはそもそもRoswellを調べなきゃ、いかんのと、ちゃう3?
それにしてもcl-dbiで返ってきたデータにはめんくらった。
こんな感じのlistになっていたからだ。
(:|date| 3281472000 :|place| "A7" :|count| 3 :|day| 7 :|raceno| 1 :|umano| 1 :|name| "アドマイヤマドンナ" :|sex| "牝" :|age| 2 :|time| 1137/10 :|umaorder| 13 :|odds| 502/5 :|ninki| 13 :|stable| "南井 克巳" :|weight| 54 :|forecast| :NULL)
中身のことは別にどうでもいい1。
「:|date|」の部分はテーブルのカラム名なのだろう、ということはわかるけれど、「:」と「|」には何か、意味があるのか? そもそもこんな形式で? mapcarで舐めてdateの次のものを値とあつかうなんて面倒じゃないか?
たとえば、pg.elだと((data . 3281472000)(place . "A7")….)みたいな感じで返却されてきていた。alistというらしいが、これならassoc2で簡単に値を求めることができる。それがなんでまたこんな形式に?
alistに変換する関数を書くか、と考えながらあちらこちら調べていたらgetf? 何、それ? おいしいの?
返却されてきたlistは実はplistというものらしい。
(getf list :|date|)
で、値を求めることができた。
getfは素のelispにはなく、cl.elに定義されていた。
でも「:」と「|」の謎は解けていないのだった。
とりあえず、昔、インストールしておいたSLIMEでREPLは立ち上がる。
まず、CommonLispからDBアクセスはどうしたらよいのか。
対象はPostgreSQL。ググったら「CommonLispとRaspberryPiでデータベース~その1~」がひっかかった。つらつら。なるほど「cl-dbi」1というものがあるらしい。
さっそく例題にしたがってやってみる。
あっさりとデータが返ってきた2。
へえ。
コードをながめる。
;; Quicklispで「cl-dbi」をロード
(ql:quickload :cl-dbi)
;; パッケージ定義
(defpackage psql-package
(:use :cl
:cl-dbi))
;; パッケージに入る
(in-package :psql-package)
;; データベースへ接続
(defvar connection
(dbi:connect :postgres
:database-name "test"
:username "nobody"
:password "1234"))
;; クエリを実行
(let* ((query (dbi:prepare connection "SELECT * FROM hello"))
(result (dbi:execute query)))
(loop for row = (dbi:fetch result)
while row
do (format t "~A~%" row)))
——packageってなんぞ?
いや、そういうものがあるらしいということは知っていたけど、腑に落ちてはいなかった。「in-package」があるってことは「out-package」もあるのか?3
elispにはそんなものないし。
ここでようやく気づく。
思ったより敷居が高いぞ、CommonLisp。
オライリー本が必要だ。でもM.D. ConradBarsk「Land of Lisp」ぐらいしか、ないよなぁ。そういえば、ポール・グレアムの「ANSI CommonLisp」をもっていたっけ。package。package。ふむふむ。よくわからん。でもなんとなく、わかった。
やってみたところ、packageの中にpackageをつくるようなことはできないようだった4。平面にぼこぼこと溜池があるようなイメージかな。「in-package」というより「change-package」?
いずれにしても1ファイル1パッケージにした方がよさそうだ。
作者は日本人でLispハッカーとして有名な人だった。
その直後、pg.elが動かなくなっていることに気づいてあひょひょひょ〜、となった。
なかった。
CL-USER> (defpackage abc (:use :cl)) #<PACKAGE "ABC"> CL-USER> (in-package :abc) #<PACKAGE "ABC"> ABC> (defpackage def (:use :cl)) #<PACKAGE "DEF"> ABC> (in-package :def) #<PACKAGE "DEF"> DEF> (defun wrk() 999) WRK DEF> (in-package :cl-user) #<PACKAGE "COMMON-LISP-USER"> CL-USER> (def::wrk) 999 CL-USER> (abc::def::wrk) ; Evaluation aborted on #<SB-INT:SIMPLE-READER-ERROR "too many colons in ~S" {1005C4D243}>. CL-USER>
というわけで、何年も前にインストールしていたCommonLispを使いはじめた。EmacsのSLIMEからちょこちょこいじってみたのだけど、ショックなことにslime-autodoc-modeがオンになっていると、空白キーがコンフリクトしてしまってSKKで漢字変換ができない。ひらがなは入力できるけど。
local-set-keyで両方を呼ぶような関数をでっちあげて空白キーに割り当てただけど、どういうわけか、いつのまにか、slime-autodoc-spaceに上書きされてしまう。
うーむ。
まったくの出来心からMacBook AirのEmacsをバージョンアップしてしまった。Emacs24からEmacs25へ。いつものなら動かなくなってしまうのが嫌で極力、バージョンアップはしないようにしているのだが。パソコンを買い替えるタイミングで最新にするようにしていた。
ついでに、と思ってHomwbrewをupgradeしたのが、そもそもまちがい。
EmacsもPostgreSQLも何もかも動かなくなってしまった。Homebrewのパッケージのぼろぼろになってしまい、リカバリーするのに何日もかかってしまう——いっそ、TimeMachineで元に戻してしまおうか、と思ったくらい。
ようやく元にもどった、と安心したのだが、EmacsからPostgreSQLへアクセスできなくなっていることが判明。pg.elからPostgreSQL10へのアクセスがプロトコルの問題でできなっていた。pg.elのバージョンアップは止まったまま……。
はまった。
PostgreSQLのデグレーションにも失敗し。
かっとなって。
psqlを起動してPostgreSQLのデータを取得する関数をつくる。
Ubuntuの画面共有にMacから接続できない。
ディスプレイってけっこう邪魔だからVNC接続できるようにしておきたいんだが。——MacのVNCもだめだし、TigerVNCもだめで、RealVNCからもだめ。RaspberryPiへは接続できるんだからVNCサーバを同じものにしてやれば、いいか、と思ってふと思う。そもそも、UbuntuのVNCサーバって何?
dpkg -l
で、見てみたら「Vino」とかいうやつだった。
それでググってみたら理由がわかった。デフォルトでは暗号化して通信しているらしい。解決策も判明。
gsettings set org.gnome.Vino require-encryption false
無事、Macの「サーバへ接続」から接続可能になった。
ところが、Beelink S1で画面が開いていないと、VNCの画面は真っ暗なのだった……。やはりVNCサーバを切り替えるしかないみたいだ、結局。