Lambdaカクテル

京都在住Webエンジニアの日記です

Invite link for Scalaわいわいランド

Common Lispで直線と交点を描画する(lispbuilder-sdl)

f:id:Windymelt:20180918232742p:plain

交差するときは丸を表示する

f:id:Windymelt:20180918232757p:plain

交差しないときは表示しない

こういうのを作った

mf-atelier.sakura.ne.jp

上記サイトを参考にして,固定した線分と,一点がマウスで操作できる線分との交点を表示するコードを書いた. 図形描画ライブラリには lispbuilder-sdl を用いた.lispbuilder-sdlの操作には,Common LispでSTGを作りますが何か?を参考にした.

rosファイルを以下に示す.Roswellが動作する環境であれば,これを保存し,ros hogehoge.ros で実行できる.

#!/bin/sh
#|-*- mode:lisp -*-|#
#|
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp (ql:quickload '(:cffi :lispbuilder-sdl :lispbuilder-sdl-binaries) :silent t)
  )

(defpackage :ros.script.ray.3746070076
  (:use :cl))
(in-package :ros.script.ray.3746070076)

;;; helper functions
(defun line-cross (l1ax l1ay l1bx l1by l2ax l2ay l2bx l2by)
  (let* ((ksi (- (* (- l2by l2ay)
                   (- l2bx l1ax))
                (* (- l2bx l2ax)
                   (- l2by l1ay))))
        (eta (- (* (- l1bx l1ax)
                   (- l2by l1ay))
                (* (- l1by l1ay)
                   (- l2bx l1ax))))
        (delta (- (* (- l1bx l1ax)
                     (- l2by l2ay))
                  (* (- l1by l1ay)
                     (- l2bx l2ax))))
        (lambda* (/ ksi delta))
        (mu (/ eta delta)))
    (if (and (and (>= lambda* 0)
                  (<= lambda* 1))
             (and (>= mu 0)
                  (<= mu 1)))
        `(t ,(+ l1ax (* lambda* (- l1bx l1ax)))
             ,(+ l1ay (* lambda* (- l1by l1ay))))
        nil)))

(defun main (&rest argv)
  (declare (ignorable argv))
  (sdl:with-init ()
    (sdl:window 480 480 :title-caption "ray")
    (setf (sdl:frame-rate) 60)

    (sdl:update-display)

    (sdl:with-events ()
      (:quit-event () t)
      (:key-down-event (:key key)
       (when (sdl:key= key :sdl-key-escape)
         (sdl:push-quit-event)))
      (:idle ()
       (sdl:clear-display sdl:*black*)
       (render)
       (sdl:update-display)))))

(defun render ()
  (sdl:draw-line (sdl:point :x (sdl:mouse-x) :y (sdl:mouse-y)) (sdl:point :x 64 :y 64) :color sdl:*white*)
  (sdl:draw-line (sdl:point :x 0 :y 128) (sdl:point :x 512 :y 128))
  (let ((crossing (line-cross (sdl:mouse-x) (sdl:mouse-y) 64 64 0 128 512 128)))
    (unless (null crossing)
      (sdl:draw-circle (sdl:point :x (cadr crossing) :y (caddr crossing)) 10 :color sdl:*red*)))
  )
;;; vim: set ft=lisp lisp:

openSuSE Tumbleweedでlemを動かす(ncursesトラブル)

lemを動かすのにちょっと苦労したのでメモ.

TL;DR

  • ros install cxxxr/lem
  • vi ~/.roswell/lisp/quicklisp/dists/quicklisp/software/cl-charms-****/src/low-level/curses-bindings.lisp
    • "libncursesw.so.6"を読み込んでいる箇所をコメントアウトし,libncursesw.so.5が読み込まれるようにする
  • lem

lemとは

lemとは,Common Lispで書かれたEmacsライクなエディタである.

github.com

Common Lispで設定を記述することができ(拡張を書くことができ),内部でCommon Lisp処理系を起動したり,外部で起動したCommon Lisp処理系にSWANKプロトコルで接続することもできる,Common Lispとの相互運用を意識した高機能なエディタである. また,よく使う機能(pareditやシンタックスハイライト,auto-complete)は最初から移植されており,いきなり使えるようになっている.

利用開始時にコードや拡張がコンパイルされるため,かなり高速に起動するのが特徴である.

lemが動かない

自分もこれをインストールしようと思っていたが,ncursesまわりのトラブルで起動すらできなかった.以下のようなエラーだ.

% ros install lem
% lem
Unhandled SIMPLE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING{10005305B3}>:
Trying to access undefined foreign variable "COLORS".
0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002C84F93}> #<unused argument> :QUIT T)
1: (SB-DEBUG::RUN-HOOK SB-EXT:*INVOKE-DEBUGGER-HOOK* #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002C84F93}>)
2: (INVOKE-DEBUGGER #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002C84F93}>)
3: (ERROR "Trying to access undefined foreign variable ~S." "COLORS")
4: (CFFI::FS-POINTER-OR-LOSE "COLORS" CL-CHARMS/LOW-LEVEL::LIBCURSES)
5: (CL-CHARMS/LOW-LEVEL::%VAR-ACCESSOR-*COLORS*)
6: (LEM.TERM:TERM-INIT)
7: ((:METHOD LEM-INTERFACE:INVOKE (LEM-NCURSES::NCURSES T)) #<unused argument> #<CLOSURE (LAMBDA (&OPTIONAL LEM::INITIALIZE LEM::FINALIZE) :IN LEM:LEM) {1002C8446B}>) [fast-method]
8: (SB-INT:SIMPLE-EVAL-IN-LEXENV (APPLY (QUOTE MAIN) ROSWELL:*ARGV*) #<NULL-LEXENV>)
9: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) #<NULL-LEXENV>)
10: (SB-EXT:EVAL-TLF (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) NIL NIL)
11: ((LABELS SB-FASL::EVAL-FORM :IN SB-INT:LOAD-AS-SOURCE) (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) NIL)
12: (SB-INT:LOAD-AS-SOURCE #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> :VERBOSE NIL :PRINT NIL :CONTEXT "loading")
13: ((FLET SB-FASL::THUNK :IN LOAD))
14: (SB-FASL::CALL-WITH-LOAD-BINDINGS #<CLOSURE (FLET SB-FASL::THUNK :IN LOAD) {7F757CD5753B}> #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}>)
15: ((FLET SB-FASL::LOAD-STREAM :IN LOAD) #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> NIL)
16: (LOAD #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> :VERBOSE NIL :PRINT NIL :IF-DOES-NOT-EXIST T :EXTERNAL-FORMAT :DEFAULT)
17: ((FLET ROSWELL::BODY :IN ROSWELL:SCRIPT) #<SB-SYS:FD-STREAM for "file /home/windymelt/.roswell/bin/lem-ncurses" {1002BB1AE3}>)
18: (ROSWELL:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses")
19: (ROSWELL:RUN ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))
20: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROSWELL:RUN (QUOTE ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))) #<NULL-LEXENV>)
21: (EVAL (ROSWELL:RUN (QUOTE ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))))
22: (SB-IMPL::PROCESS-EVAL/LOAD-OPTIONS ((:EVAL . "(progn #-ros.init(cl:load \"/usr/local/etc/roswell/init.lisp\"))") (:EVAL . "(ros:run '((:eval\"(ros:quicklisp)\")(:script \"/home/windymelt/.roswell/bin/lem-ncurses\")(:quit ())))")))
23: (SB-IMPL::TOPLEVEL-INIT)
24: ((FLET SB-UNIX::BODY :IN SB-EXT:SAVE-LISP-AND-DIE))
25: ((FLET "WITHOUT-INTERRUPTS-BODY-27" :IN SB-EXT:SAVE-LISP-AND-DIE))
26: ((LABELS SB-IMPL::RESTART-LISP :IN SB-EXT:SAVE-LISP-AND-DIE))
unhandled condition in --disable-debugger mode, quitting
Unhandled SIMPLE-ERROR in thread #<SB-THREAD:THREAD "main thread" RUNNING
                                    {10005305B3}>:
  Trying to access undefined foreign variable "stdscr".
Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {10005305B3}>
0: (SB-DEBUG::DEBUGGER-DISABLED-HOOK #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002CB4D53}> #<unused argument> :QUIT T)
1: (SB-DEBUG::RUN-HOOK SB-EXT:*INVOKE-DEBUGGER-HOOK* #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002CB4D53}>)
2: (INVOKE-DEBUGGER #<SIMPLE-ERROR "Trying to access undefined foreign variable ~S." {1002CB4D53}>)
3: (ERROR "Trying to access undefined foreign variable ~S." "stdscr")
4: (CFFI::FS-POINTER-OR-LOSE "stdscr" CL-CHARMS/LOW-LEVEL::LIBCURSES)
5: (CL-CHARMS/LOW-LEVEL::%VAR-ACCESSOR-*STDSCR*)
6: (LEM.TERM:TERM-FINALIZE)
7: ((FLET "CLEANUP-FUN-2" :IN LEM-INTERFACE:INVOKE)) [cleanup]
8: ((:METHOD LEM-INTERFACE:INVOKE (LEM-NCURSES::NCURSES T)) #<unused argument> #<CLOSURE (LAMBDA (&OPTIONAL LEM::INITIALIZE LEM::FINALIZE) :IN LEM:LEM) {1002C8446B}>) [fast-method]
9: (SB-INT:SIMPLE-EVAL-IN-LEXENV (APPLY (QUOTE MAIN) ROSWELL:*ARGV*) #<NULL-LEXENV>)
10: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) #<NULL-LEXENV>)
11: (SB-EXT:EVAL-TLF (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) NIL NIL)
12: ((LABELS SB-FASL::EVAL-FORM :IN SB-INT:LOAD-AS-SOURCE) (ROSWELL:QUIT (APPLY (QUOTE MAIN) ROSWELL:*ARGV*)) NIL)
13: (SB-INT:LOAD-AS-SOURCE #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> :VERBOSE NIL :PRINT NIL :CONTEXT "loading")
14: ((FLET SB-FASL::THUNK :IN LOAD))
15: (SB-FASL::CALL-WITH-LOAD-BINDINGS #<CLOSURE (FLET SB-FASL::THUNK :IN LOAD) {7F757CD5753B}> #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}>)
16: ((FLET SB-FASL::LOAD-STREAM :IN LOAD) #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> NIL)
17: (LOAD #<CONCATENATED-STREAM :STREAMS NIL {1002BB34B3}> :VERBOSE NIL :PRINT NIL :IF-DOES-NOT-EXIST T :EXTERNAL-FORMAT :DEFAULT)
18: ((FLET ROSWELL::BODY :IN ROSWELL:SCRIPT) #<SB-SYS:FD-STREAM for "file /home/windymelt/.roswell/bin/lem-ncurses" {1002BB1AE3}>)
19: (ROSWELL:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses")
20: (ROSWELL:RUN ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))
21: (SB-INT:SIMPLE-EVAL-IN-LEXENV (ROSWELL:RUN (QUOTE ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))) #<NULL-LEXENV>)
22: (EVAL (ROSWELL:RUN (QUOTE ((:EVAL "(ros:quicklisp)") (:SCRIPT "/home/windymelt/.roswell/bin/lem-ncurses") (:QUIT NIL)))))
23: (SB-IMPL::PROCESS-EVAL/LOAD-OPTIONS ((:EVAL . "(progn #-ros.init(cl:load \"/usr/local/etc/roswell/init.lisp\"))") (:EVAL . "(ros:run '((:eval\"(ros:quicklisp)\")(:script \"/home/windymelt/.roswell/bin/lem-ncurses\")(:quit ())))")))
24: (SB-IMPL::TOPLEVEL-INIT)
25: ((FLET SB-UNIX::BODY :IN SB-EXT:SAVE-LISP-AND-DIE))
26: ((FLET "WITHOUT-INTERRUPTS-BODY-27" :IN SB-EXT:SAVE-LISP-AND-DIE))
27: ((LABELS SB-IMPL::RESTART-LISP :IN SB-EXT:SAVE-LISP-AND-DIE))
unhandled condition in --disable-debugger mode, quitting

まとめると,COLORSstdscrという変数にアクセスできなかったようだ.

lemは,複数の画面描画手段を使うことができるが,そのうちの標準的な手段がncursesである.これを使って画面制御を行うのだが,ncursesはC言語ライブラリなので直接Common Lispから呼び出すことができない. Common LispからC言語ライブラリを呼び出すといった,言語感のプログラムの呼び出しにはFFI(Foreign Function Interface)という仕組みを使う. またCommon Lisp上で動作するFFI実装として,CFFIという著名なモジュールがあり,lemでもこのモジュールを経由してncurses等の外部ライブラリを呼び出している. 今回のエラーも,CFFIによってncurses上で定義されているはずの変数を呼び出そうとしたが,該当する変数が存在しなかった,というエラーである.

どこが問題だったのか

実は不明である.

これはncursesの互換性の問題らしい.らしいというのは,リリースノートを見たが具体的な変更がどこかよく分からなかったからだ.

lem側の問題なのか,どちらかといえばncurses側の問題なのかを切り分けるのに時間がかかってしまったが,後述するcl-charmsのexampleを走らせたところエラーが生じたことから,ncurses(のラッパ)で問題が生じていることがわかった.

解決策

ncurses5を利用する.

これは全く天下り的な方法で発見した.つまり適当に読み込むライブラリのバージョンを5にしたら動いたというだけの話で,どこが問題なのかはよくわからなかった. ともかく,ncurses5では動作したので,これを使えばいい.

ところでopenSuSE Tumbleweedはncurses6を標準的に採用しているので,ncurses5だけ使うわけにはいかない.今回は,lemが呼び出しているライブラリをncurses5に制限するという方法で問題を回避する.

cl-charms

lemは,FFIを介して直接ncursesを使うのではなく,cl-charmsというncursesのラッパーライブラリを使うことで画面操作を行う. このcl-charmsはコード中にCFFIncursesを読み込む箇所がある.これを以下に示す.

#+unicode
(cffi:define-foreign-library libcurses
  (:darwin (:or "libncurses.dylib" "libcurses.dylib"))
  (:unix (:or "libncursesw.so.6"
              "libncursesw.so.5"
              "libncursesw.so.14.0"
              "libncursesw.so"))
  (:windows (:or "pdcurses" "libcurses"))
  (t (:default "libcurses")))

#-unicode
(cffi:define-foreign-library libcurses
  (:darwin (:or "libncurses.dylib"
                "libcurses.dylib"))
  (:unix (:or "libncursesw.so.6"        ; XXX: is this the right thing
                                        ; to load? Should we also add
                                        ; libncursesw.so as a
                                        ; fallback?
              "libncurses.so.6"
              "libncurses.so.5"
              "libncursesw.so.14.0"
              "libncurses.so"
              "libcurses"))
  (:windows (:or "pdcurses"
                 "libcurses"))
  (t (:default "libcurses")))

これを見ればわかるとおり,cl-charmsはまずOSを判別し,その後でncursesを読み込んでいく.UNIXの場合その優先順位は,

  1. libncursesw.so.5
  2. libncursesw.so.6
  3. libncursesw.so.14.0
  4. libncursesw.so

である.これらのうちどれかが発見されれば,それが読み込まれる.

ちなみに#+unicode#-unicodeとしている箇所は,処理系のUnicode対応によって分岐している箇所である*1.Unicode対応であればlibncurseswが使われるが,そうでない場合はlibncursesが使われるようだ(なぜかlibncurseswを読み込む箇所もあるが).

また,roswellでlemをインストールする場合は,cl-charms~/.roswell/lisp/quicklisp/dists/quicklisp/software/cl-charms-****/src/low-level/curses-bindings.lispに保存されているので,これを書き換えればよい.

このファイルが存在しない場合は,ros runして(ql:quickload :cl-charms)などとしてダウンロードさせるとよい.

libncurses6を読み込まないようにする

さて,ncurses6を読み込まないようにする.手順は至って簡単で,libncursesw.so.6libncurses.so.6となっている箇所をすべてコメントアウトすればよい.実施後を以下に示す.

#+unicode
(cffi:define-foreign-library libcurses
  (:darwin (:or "libncurses.dylib" "libcurses.dylib"))
  (:unix (:or ;"libncursesw.so.6"       ; ここ
              "libncursesw.so.5"
              "libncursesw.so.14.0"
              "libncursesw.so"))
  (:windows (:or "pdcurses" "libcurses"))
  (t (:default "libcurses")))

#-unicode
(cffi:define-foreign-library libcurses
  (:darwin (:or "libncurses.dylib"
                "libcurses.dylib"))
  (:unix (:or ;"libncursesw.so.6"       ; ここ ; XXX: is this the right thing
                                        ; to load? Should we also add
                                        ; libncursesw.so as a
                                        ; fallback?
              ;"libncurses.so.6"        ; ここ
              "libncurses.so.5"
              "libncursesw.so.14.0"
              "libncurses.so"
              "libcurses"))
  (:windows (:or "pdcurses"
                 "libcurses"))
  (t (:default "libcurses")))

3点コメントアウトした.これでncurses6cl-charmsからは読み込まれなくなった.

後は,通常通りlemと実行するだけでlemが起動する.

うまくいかないときは,ros install lemとすることでダンプされたバイナリが削除され,再度lemがビルドできるようになる.

まとめ

ncurses6が足枷になっているとは思いもよらなかったが,あまりncurses6は人気がないのだろうか. もとよりcl-charmsは原作者がメンテナンスを放棄したものを有志がメンテしている状況なので,あまり開発が活発でないという側面があることも付け加えておきたい.

便利な資料

blog.8arrow.org

qiita.com

qiita.com

*1:Feature expression, 処理系が対応している機能によって,式読み取り時に式を分岐させるための記述CLHS: Section 24.1.2.1

Common Lisp困りどころ その2: ClozureCLのファイルパスのエンコーディング

台風の風がするどい.実家の九州では珍しくもないことだが,関西でこれほどの風が吹くのは久しぶりだ.

さて今回ClozureCLの日本語処理でハマったのでメモしておく.

シチュエーション

  • OSはFreeBSD 11.1
  • ファイルシステムはZFS
  • 処理系はClozureCL 1.11(roswellでインストールした.ccl-bin/1.11)
  • ファイルリストをuiop/filesystem:subdirectoriesまたはuiop/filesystem:directory-filesで取得した.
  • これをWebフレームワークであるcaveman2で表示しようとしたところ,文字化けが発生した.
  • プロキシとしてnginxを挟んでいる.

問題調査

まずnginxの問題を疑ったが,問題ないことがわかった.caveman2が表示するテンプレートに日本語を混ぜたところ,問題なく表示できたからである.

またファイル名自体が文字化けしているのではないかと疑ったが,問題ないことがわかった.sshによって直接ファイルリストを取得してみたが,正常に表示されたからである.

ここで文字化けしている文字列がどのように化けているのか確認するために 文字化け解読ツール「もじばけらった」 を使用した.すると,これをlatin1で再度エンコードしてUTF-8でデコードすると正常に表示できることがわかった.

「latin1 clozurecl」でググっていると以下のようなページに辿り着いた.

t2ru.hatenablog.jp

どうもClozureCLに共通する問題らしい.もしやと思い,ファイルリストを取得する箇所に以下のようなコードを挿入した.

;; before
(defvar pathstr "/foo/bar/")
(defvar path (pathname pathstr))

;; 文字列がほしいのでenough-namestringを使う
;; namestringでよかったかも?
 (mapcar #'enough-namestring (directory-files pth))
 (mapcar #'enough-namestring(subdirectories pth))
;; after

;; 変換関数
(defun ensure-encoding-in (s)
  ; cclの場合にのみ,一度文字列をlatin1でオクテット列に戻し,utf-8でデコードしなおす
  ; '外の世界'から入ってきたパス名に適用する
  #+ccl
  (setf s (babel:octets-to-string (babel:string-to-octets s :encoding :latin1) :encoding :utf-8))
  s)
(defun ensure-encoding-out (s)
  ; cclの場合にのみ,一度文字列をutf-8でオクテット列に戻し,latin-1でデコードしなおす
  ; '内の世界'から出ていくパス名に適用する
  #+ccl
  (setf s (babel:octets-to-string (babel:string-to-octets s :encoding :utf-8) :encoding :latin1))
  s)

(defvar pathstr (ensure-encoding-out "/foo/bar/"))
(defvar path (pathname pathstr))

(mapcar 'string #'ensure-encoding-in (mapcar #'enough-namestring (directory-files pth)))
(mapcar 'string #'ensure-encoding-in (mapcar #'enough-namestring (subdirectories pth)))

すると正常にファイル一覧を得ることができた.よかったですね.

なんでやねん

どうしてこうなるのか調査しようとしたが,自分の やり方が不十分なのか,clozureclがおちゃめなのかまだ分かっていない.

そもそもccl:*default-file-character-encoding*とかをちゃんと設定したら動くんちゃう?という気もするが,今日は気力が尽きてしまった.

Common Lisp困りどころ その1: `package-inferred-system`

Common Lispは良い言語だが,初心者が必ず陥る困りどころがいくつかあり,これで出鼻を挫かれるようなことがあってはいけないと思っている. そこで,この困りどころを提示することによって,よりよい解決方法や回避策がインターネットを通じて集ってくるのをうながそうという試みとして,この「 #CommonLisp困りどころ 」シリーズをやることにした.

twitter.com


Common Lispのパッケージ管理における革新であるpackage-inferred-systemについて,ちょっとした解説と,不満点を書き出してみる. もしよければ手をさしのべてほしい.

パッケージの導入

まず一般的なプログラミングの技法として,開発しているソフトウェアが複雑になった場合,モジュールとしてソフトウェアを分割するのが普通である.Common Lispにおいて,モジュールの単位はパッケージpackageである. パッケージに分離させた上で,パッケージが別のパッケージのシンボルをインポートしたり,自分のパッケージのシンボルをエクスポートしたりする. そして通常ならばファイルもこれに応じて分離するだろう.

ASDFにおけるコンパイル

ところでCommon Lispで標準的なビルドツールであるASDFを使ってプロジェクト(ASDFの用語ではシステムsystem)を実行しようとするとき,基本的にまずプロジェクトを構成するファイル同士の依存関係が解決され,その後で全てのファイルが一定の順序にコンパイルされていく. つまり動作時に動的にファイルがコンパイルされ読み込まれるということが基本的にない.実行時には全てのパッケージがコンパイルされている必要がある*1わけだ.このあたりはPythonなどのスクリプト言語とは趣が違うところだ. 関数の類を呼び出す際に存在しているかどうかが確認されるのではなく,コンパイル時点で参照できなければならないのだ*2

コンパイルを正常に行うためには,依存関係を記述する必要がある

つまり,間違った順序でソースコードをコンパイルすると,コンパイルに失敗する.必ず正しく依存関係に適った順序でコンパイルを行う必要がある. したがって,コンパイルをシステマチックに行うため,開発者はファイル(パッケージ)同士の依存関係をプロジェクトトップに配置されたプロジェクト管理用のファイルプロジェクト名.asd(ASDファイル)に記述する必要がある.

依存関係記述は面倒だが,ASDFはそれをやらない

ASDFにおける依存関係の記述はとにかく面倒な代物で,構成ファイル数が片手の指に入る間はどうということないのだが,ファイル数が増えるにつれてファイル間の依存関係が爆発し,人間には制御できない代物になることは想像に難くない.こんなことは人間のすることではない.だがASDFはこれを自動的には行ってくれないのだ.なぜなのか理由を考えてみよう.

解決不可能なのはなぜか

ファイル同士の依存関係が解決できないというのを言い換えると,ファイルをコンパイルするために別のどのファイルがコンパイルされているべきかが分からない,ということである. ファイルにはいくつかのパッケージがあり,それらは別のパッケージに依存している.だがパッケージ名からファイルに至るパスを推測(infer)できないので,そのパッケージがどのファイルにあるかは分からないのである.

one package one file same nameの導入

この問題,ファイル同士の依存関係を自動的に解決できない問題は,ファイル名とパッケージ名を統合し,1ファイル1パッケージとする規約を導入することで解消することができる. パッケージ名からファイル名を推測することができるようになるからである.そして必然的に1ファイルには複数パッケージを置くことができなくなる. One package, One file, Same nameである.これを実現するのがASDFの拡張機能であるpackage-inferred-systemである.直訳するとパッケージ推測型システムといったところか. package-inferred-systemでは,sampleprojectというASDFプロジェクトがあるとき,foo/barというパッケージをuse-packageなどで読み込んでいると,ASDFはsampleproject.asdがあるディレクトリを起点として./foo/bar.lispを参照してくれる.

不満点

この仕組みはいたってよくできていて,複数のパッケージからなるシステムを書く際の負担をとても軽くしてくれる.ファイル名を考えなくていいところがとてもモダンに感じられる. ただ不満な点があり,筆者はこれを解消できていないため,識者のアドバイスを求めたいと思う.それは・・・

ファイル名解決の起点となるパスを変更できないか?ということである.つまりこういうことである.

現代的なプロジェクトでは,実際的にはソースコードはsrc/といったサブディレクトリに押し込められ,これに並んでscripts/documents/といったサブディレクトリが並ぶことが多い.この場合でも,sampleproject.asdといったASDファイルをプロジェクトルートに置くほうが都合が良いこともある. 例えばテストをすることを考えると,テスト用ASDファイルから本体システムを呼び出すには,両者のASDファイルが同じ場所にあるほうが読み込みの都合が良い.

この場合,package-inferred-systemではファイル名とパッケージ名が統合され,ファイルはASDファイルを起点として推測されるので,src/foo/bar.lispのパッケージ名はsrc/foo/barとなってしまうのである!こんなのありえない!ださすぎる!

これを回避するためにASDファイルを移動するという手段は前述の理由により避けたい.ファイル名を推測するための起点となるディレクトリを./srcにして,foo/barsrc/foo/bar.lispを参照してくれるようにならないだろうか?

・・・といった話をtwitterでしていると,親切な方がいろいろ教えてくれた.残念ながら,実践してみてもうまくいかなかった.どこに指定すればよかったのだろう.

なんとかモダンなCommon Lisp開発環境を手にしたいので,誰か助けてほしい!!!

Related entries

ASDF - Another System Definition Facility

blog.3qe.us

octahedron.hatenablog.jp

qiita.com

require, ASDF, quicklispを正しく使う | κeenのHappy Hacκing Blog

*1:SBCL,CCLといったネイティブコンパイラの場合の話.別の処理系ではまた事情が異なるかもしれない.

*2:このあたりの理解はあいまいなので,間違いや補足があれば指摘してほしい

Linking emacs SLIME and docker-compose with SWANK server -- modern Common Lisp development

Better translation here: https://kvantos.github.io/#articles_en/emacs-slime/

Emacs works quite well with Common Lisp.

We can use REPL and autocompletion feature connecting to new Common Lisp implementation or working implementation; SLIME server booted on implementation holds communicaiton between implementation and Emacs.

Though frequently we connect into working implementation on local environment with SLIME and that is well-documented on the Web, there is few documents that describes a way to connect Emacs into working implementation and enjoy productive development in modern environment -- Docker compose environment.

Thus I try to:

  • Connect emacs into living typical Common Lisp application (Caveman 2) booted on docker-compose.
  • Interact with living code and interfare with its behavior.

I assume you have already done with basic SLIME configuration on Emacs.

Motivation

My hobby program written in Common Lisp is dockerized and able to boot with docker-compose, except "editor and its environment". So situation is this: I connected Emacs into the local-booted implementation with SLIME for syntax highlight and autocompletion, not into the docker-compose-booted implementation. It raised many probrems such that malfunction of autocompletion and loading library, and burdened developer of maintaining develop environment.

Now I am going to harmonize coding environment and server environment to develop with genuine living code.

Environment

  • cat /proc/version
    • Linux version 4.4.104-39-default (geeko@buildhost) (gcc version 4.8.5 (SUSE Linux) ) #1 SMP Thu Jan 4 08:11:03 UTC 2018 (7db1912)
  • lsb_release -a
    • LSB Version: n/a
    • Distributor ID: openSUSE project
    • Description: openSUSE Leap 42.3
    • Release: 42.3
    • Codename: n/a

ros version

build with gcc (SUSE Linux) 4.8.5
libcurl=7.37.0
Quicklisp=2017-03-06
Dist=2017-08-30
lispdir='/usr/local/etc/roswell/'
homedir='/home/windymelt/.roswell/'
configdir='/home/windymelt/.roswell/'

ros run -- -version

SBCL 1.4.0

Sample application

I prepared the sample application. You can try to connect into the application with SWANK.

github.com

Let docker-compose expose SWANK port

SWANK server should be booted on an implementation to communicate with Emacs.

We expose port used for SWANK server through host to allow connect from Emacs.

Here we suppose that the implementation is working on app service built from following Dockerfile. Web application (caveman 2) and SWANK server use 5000 port and 6005 port respectively. You can choose arbitary port number for SWANK server because Emacs always asks for SWANK host and port number.

# docker-compose.yml
version: '3'
services:
  app:
    build: .
    ports:
      - "6005:6005" # For development; SWANK port. cf. Dockerfile
      - "5000:5000"

Dockerfile

In Dockerfile we command CMD to boot SWANK server first, preceding application server booting.

We look at bottom part of Dockerfile because many part of Dockerfile have no buissiness with SWANK:

CMD [ \
  "qlot", "exec", "ros", \
  "-e", "(ql:quickload :swank)", \
  "-e", "(setf swank::*loopback-interface* \"0.0.0.0\")", \
  "-e", "(swank:create-server :port 6005 :dont-close t :style :spawn)", \
  "-l", "bundle-libs/bundle.lisp", \
  "-S", ".", "~/.roswell/bin/clackup", "--server", ":woo", "--address", "0.0.0.0", "--port", "5000", "app.lisp" \
]

The container boots qlot, library localiser. It makes loading libraries under quicklisp/.

qlot has responsibility to pass configuration about project-local libraries to implementation and the implementation is booted by ros. ros processes dangling options in written order:

First swank is loaded by -e (ql:quickload :swank) to prepare booting SWANK server. You may declare dependency into qlfile and write -e (asdf:load-system :swank) here.

Then we overwrite the address which SWANK server listens by -e (setf swank::*loopback-interface* "0.0.0.0"). This is because docker environment often makes turbulance of IP addresses. We ensure connecting to implementation by fixing listening address to 0.0.0.0.

By the way SWANK has no authentication mechanism so everyone can connect to socket of SWANK server. You should use SSH if you have to secure connection, but it's okay because this is development purpose.

Next we boot SWANK server listening 6005 port by -e (swank:create-server :port 6005 :dont-close t :style :spawn). :dont-close should keep connection but I'm not sure. :style :spawn designates type of connection: one spawned process per connection. It is not needed if you connect SWANK server from exactly one client.

Following processes have no business with SWANK.

-l bundle-libs/bundle.lisp is to load dependent local library. qlot can freeze all dependent libraries into bundle-libs executing qlot bundle and bootstrapping file to load libraries is bundle.lisp. It should be used by ros -l option. -l simply loads and executes specified file.

-S . configures ASDF default system repository (asdf:*central-registry*) into current directory (this time it's project-root). ASDF can find system such that its ASD file is placed at specified directory.

At last we boot clack application by ~/.roswell/bin/clackup --server :woo --address 0.0.0.0 --port 5000 app.lisp. app.lisp is called clack aplication alike perl's app.psgi. This file is created automatically when Caveman project is created.

Connecting from Emacs

It's time to flight. Boot project with docker-compose and connect from Emacs.

docker-compose up

After M-x slime-connect you will get the prompt Host: to specify localhost. Then you will get the prompt Port: to specify 6005 configured above. After all following output are made into minibuffer and you can see working REPL window.

Connecting to Swank on port 6005..
Connected. Take this REPL, brother, and may it serve you well.

You can move into your package because living codes are already loaded.

;; there is Caveman 2 project named :hogeapplication
CL-USER> (in-package :hogeapplication.web)
#<PACKAGE "HOGEAPPLICATION.WEB">

HOGEAPPLICATION.WEB> *web*
#<<WEB> {12345678}>

HOGEAPPLICATION.WEB> (render #P"index.html")
;; -- Template is rendered exactly same as you coded in hogeapplication.web package --

HOGEAPPLICATION.WEB> (clear-routing-rules *web*)
;; -- All of routing information is gone, application is made to say 404. try this! --

Conclusion

Now we can read/modify symbols inside living application because:

  • We booted SWANK server at Dockerfile and configured potr exposure setting
  • We connected into the implementation running over container orchestrated by docker-compose, from Emacs on host machine

We can use precise autocompletion along living application because package information is provided to SLIME from SWANK.

This post is self-translation of:

blog.3qe.us

SWANKサーバによるEmacs SLIMEとdocker-composeとの連携 -- 現代的なCommon Lisp開発

EmacsとCommon Lispとの相性は抜群に良い.

SLIMEというプラグインを使うと,Emacs上でCommon Lisp処理系を立ち上げて接続したり,既に起動している処理系に接続したりすることで,REPLや補完を動作させられる.このEmacsとCommon Lisp処理系との接続は,処理系上でSWANKサーバというのを起動することで取り持ってもらっている.

ローカルで動作している処理系にSLIMEで接続することはよく行われるし,そのためのドキュメントも充実しているようだが,しかしdocker,とくに現代的なdocker-compose環境でCommon Lispプロダクトを開発する際に,Emacsを処理系に接続し効果的な開発を行う方法についてのドキュメントはあまりみあたらなかった.

そこでこのエントリでは,docker-composeで起動した典型的なCommon Lispアプリケーション(Caveman 2)にEmacsを接続し,SWANKを介して実行中のコードに触れ,動作に干渉してみることにする.また,Emacsでの基本的なSLIMEの設定はもう済んでいるものとする.

動機

自分が細々といじっているソフトウェアがあり,これはdocker化されていてdocker-composeで起動もする.しかし開発環境のうち「エディタまわり」はdocker化されていなかった. どういうことかというと,このソフトウェアを編集するEmacsでは,シンタックスハイライトや補完のためにSLIMEを起動して処理系と接続していたが,接続していたのはdocker-compose上で動作する処理系ではなく,ローカルで起動させた処理系であった. このため思い通りに補完が動作しなかったり,ライブラリがうまく読み込まれないなど,開発者にとっては開発のためにDockerとローカルとで二重の負担を強いられる状況となっていた.

今回はこの不便を脱出するべく,コーディング環境とサーバの動作環境とを一本化し,まさに動作しているコードで開発できる環境を整えようと思う.

環境

  • cat /proc/version
    • Linux version 4.4.104-39-default (geeko@buildhost) (gcc version 4.8.5 (SUSE Linux) ) #1 SMP Thu Jan 4 08:11:03 UTC 2018 (7db1912)
  • lsb_release -a
    • LSB Version: n/a
    • Distributor ID: openSUSE project
    • Description: openSUSE Leap 42.3
    • Release: 42.3
    • Codename: n/a

ros version

build with gcc (SUSE Linux) 4.8.5
libcurl=7.37.0
Quicklisp=2017-03-06
Dist=2017-08-30
lispdir='/usr/local/etc/roswell/'
homedir='/home/windymelt/.roswell/'
configdir='/home/windymelt/.roswell/'

ros run -- -version

SBCL 1.4.0

サンプルアプリケーション

サンプルアプリケーションを用意した.手順に従うだけでSWANKで接続できるようになっている.

github.com

docker-composeにSWANKサーバのポートを露出させる

Emacsと処理系とが通信するためには,SWANKサーバが処理系で起動している必要がある. まずはその下準備として,SWANKサーバが使うポートをホストに露出し,Emacsから接続できるようにする.

ここで,処理系が動作するサービスはappであり,後述のDockerfileを用いてコンテナをビルドする. また,Webアプリケーション(caveman 2)が5000番を使用するのでホストで見られるように露出させる. そしてSWANKサーバは6005番ポートを使うことにする.これはEmacsから接続する際に指定できるので,自由に設定してよい.

# docker-compose.yml
version: '3'
services:
  app:
    build: .
    ports:
      - "6005:6005" # For development; SWANK port. cf. Dockerfile
      - "5000:5000"

Dockerfile

Dockerfileでは,アプリケーションサーバの起動に先行してSWANKサーバを起動させるべくCMDを記述する. Dockerfileの大半は今回の話題と無関係であり,長いのでここではその末尾を抜粋する.

CMD [ \
  "qlot", "exec", "ros", \
  "-e", "(ql:quickload :swank)", \
  "-e", "(setf swank::*loopback-interface* \"0.0.0.0\")", \
  "-e", "(swank:create-server :port 6005 :dont-close t :style :spawn)", \
  "-l", "bundle-libs/bundle.lisp", \
  "-S", ".", "~/.roswell/bin/clackup", "--server", ":woo", "--address", "0.0.0.0", "--port", "5000", "app.lisp" \
]

まずライブラリローカライザであるqlotを起動させる.これにより,quicklisp/以下のライブラリが読み込まれるようになる.

qlotの仕事は,ローカルに保存してあるライブラリの使うための設定を適切に行い,処理系に渡すことだ.ここで処理系はrosによって起動される.CMD上ではパラメータが長々とぶら下がっているが,rosはこれを順に処理していく.一緒に見ていこう.

まず-e (ql:quickload :swank)によってswankシステムが読み込まれる.これはサーバを起動するために必要な措置だ. 今思えば,qlotを使っているので依存関係をqlfileに記載して,ここでは-e (asdf:load-system :swank)とすべきだったかもしれない.

次に-e (setf swank::*loopback-interface* "0.0.0.0")によってSWANKがリッスンするアドレスを書き換える.Docker環境だとIPアドレスが変化しがちなため,ここで0.0.0.0に固定することで確実に接続できるようにする.

ちなみにSWANKに認証機構は備わっていないため, ソケットにアクセスできる誰からでも接続を許してしまう.これが心配ならばsshなどを経由する必要があるが,このエントリではdocker-composeを開発目的でのみ使っているので問題なしとしている.

次に-e (swank:create-server :port 6005 :dont-close t :style :spawn)によってSWANKサーバを6005番ポートで起動する.:dont-close tは勝手に接続を閉じないための設定だが,もしかすると不要かもしれない.:style :spawnは接続ごとにプロセスをspawnして複数の接続ができるようにするためのものである.1つの接続しかしないのであれば削除してよさそうだ.

ここからはSWANKとは無関係な処理である.

-l bundle-libs/bundle.lispは,依存ライブラリを読み込むためのものだ.qlotは,qlot bundleコマンドで依存ライブラリをすべてローカルのbundle-libsに落とすことができる.これらのライブラリ全てを読み込むブートストラップがbundle.lispで,ros-lオプションを使って読み込む.-lは,指示したファイルを単にロードして実行するだけのオプションである.

-S .は,ASDFのシステムに対するデフォルト読み込み地点(asdf:*central-registry*)をカレントディレクトリ(ここではプロジェクトルート)に設定するものである.ここで指定したディレクトリにASDファイルを置いておくと,ASDFはそのシステムを発見できるようになる.

最後に ~/.roswell/bin/clackup --server :woo --address 0.0.0.0 --port 5000 app.lisp としているのは,clackup(clackパッケージが提供)コマンドによってWebアプリケーションを起動するためのものだ.app.lispがClackアプリケーションと呼ばれるもので,他の言語におけるapp.psgiとかにあたると考えてよいだろう.このファイルはCaveman2がプロジェクトを作成したときに自動的に作成される.

Emacsから接続する

これで準備が整った.docker-composeを使ってプロジェクトを起動し,Emacsから接続してみよう.

docker-compose up

M-x slime-connectすると,Host:というプロンプトが表示されるのでlocalhostと入力する.次にPort:というプロンプトが表示されるので,先程設定した6005番ポートを指定する.すると以下のような表示がミニバッファに出力されて,REPLウィンドウが表示され,操作できるようになっているはずだ.

Connecting to Swank on port 6005..
Connected. Take this REPL, brother, and may it serve you well.

すでにコードが読み込まれているので,コード上のパッケージに移動できる.

;; hogeapplicationというcaveman2プロジェクトがあるという想定
CL-USER> (in-package :hogeapplication.web)
#<PACKAGE "HOGEAPPLICATION.WEB">

HOGEAPPLICATION.WEB> *web*
#<<WEB> {12345678}>

HOGEAPPLICATION.WEB> (render #P"index.html")
;; ---hogeapplication.webで同様の記述をしたときと全く同じようにテンプレートがレンダされる---

HOGEAPPLICATION.WEB> (clear-routing-rules *web*)
;; --ルーティング情報が全て消え,実行中のアプリケーションが404しか出さなくなる.試しにやってみよう--

まとめ

Dockerfile上でSWANKサーバを起動させ,適切に設定を行いポートを露出させることで, ホスト上のEmacsからdocker-compose上で動作するコンテナの処理系へと接続し, 動作中のアプリケーションのシンボルを読んだり,動作中のコードを変更できるようになった.

SWANKから得られる情報をもとにSLIMEが補完機能を提供するので,実際に動作するアプリケーションに忠実な補完が利用できるようになった.

Common Lispで木構造の一部にパターンマッチさせてみた(optima)

パターンマッチングライブラリのoptimaで遊んでみた。ちょっと応用して入れ子になったリストの一部分にパターンがマッチするか検査する処理を書いてみた。将来的にはパースしたHTMLの部分マッチさせて抽出する処理に使ってみたい。

まずこういうリストがあるとする。

'(1 2 3 4 (5 6 (7 8) 9) 0))

このリストに(6 (7 8) X) (ここでXは何でもよい)であるような部分があるかを、Common Lispで確かめてみたい。

#!/bin/sh
#|-*- mode:lisp -*-|#
#| <Put a one-line description here>
exec ros -Q -- $0 "$@"
|#
(progn ;;init forms
  (ros:ensure-asdf)
  #+quicklisp (ql:quickload '(:optima) :silent t)
  )

(defpackage :ros.script.optimatest.3735458529
  (:use :cl :optima))
(in-package :ros.script.optimatest.3735458529)

;;; ここからメインロジック

;; 探索対象のリスト
(defparameter *lis* '(1 2 3 4 (5 6 (7 8) 9) 0))

;; リストを探索する関数。マッチしなければcar、cdrを再帰的に試していく。
(defun walk (lis)
  (match lis
    ((list 6 '(7 8) _) ; この条件にマッチさせたい
     (format t "~S ;matched.~%" lis)
     t)
    ((null _)
     (format t "null. failing back...~%") nil)
    ((list) (format t "empty list. falling back...~%") nil)
    ((cons a b)
     (or
      (format t "~S ;cons. walking car...~%" lis)
      (walk a)
      (format t "~S ;car failed. walking cdr...~%" lis)
      (walk b)))))

(defun main (&rest argv)
  (declare (ignorable argv))
  (walk *lis*))
;;; vim: set ft=lisp lisp:
windymelt% ./optimatest.ros
(1 2 3 4 (5 6 (7 8) 9) 0) ;cons. walking car...
(1 2 3 4 (5 6 (7 8) 9) 0) ;car failed. walking cdr...
(2 3 4 (5 6 (7 8) 9) 0) ;cons. walking car...
(2 3 4 (5 6 (7 8) 9) 0) ;car failed. walking cdr...
(3 4 (5 6 (7 8) 9) 0) ;cons. walking car...
(3 4 (5 6 (7 8) 9) 0) ;car failed. walking cdr...
(4 (5 6 (7 8) 9) 0) ;cons. walking car...
(4 (5 6 (7 8) 9) 0) ;car failed. walking cdr...
((5 6 (7 8) 9) 0) ;cons. walking car...
(5 6 (7 8) 9) ;cons. walking car...
(5 6 (7 8) 9) ;car failed. walking cdr...
(6 (7 8) 9) ;matched.

マッチした場合は値を変数に入れて取り出すこともできるので、さらに処理していくこともできそうだ。

まとめ

  • car/cdrを再帰的に歩くことで部分マッチを検索できそう。
  • car/cdrがあっても絶対にマッチしない場合があるので、その場合の枝刈りもできそう。
    • carしかない場合など

リンク

qiita.com

github.com

★記事をRTしてもらえると喜びます
Webアプリケーション開発関連の記事を投稿しています.読者になってみませんか?