Gnuplot by scheme
gaucheの 衛生的マクロ
前回は、Lisp由来の伝統的なマクロだった。今度はscheme独自の、衛生的マクロってのをやってみる。gaucheとかのマニュアルを見ると、色々と便利なものが用意されてるけど、基本は
(define-syntax when (syntax-rules () [(_ test body ...) (if test (begin body ...))]))
これだけだ。一行目で、マクロの名前を定義。二行目で、変換のルール定義の開始。 三行目で、実際の定義。左側の式にマッチしたら、右側のように置き換えろ。
アンダーバーの所はwhenと書いても良い(けど、一行目で決まっているので冗長だ。定義は一か所の原理で、何でも良いと言うアンダーバーにするのが普通)。
右側になる … は、直前のパターンがゼロ個以上繰り返されるって意味。schemeの中に、独自に持ち込んだ、パターンマッチによる書き換えって事だ。
これだけ分かれば、書き換えは簡単だ。
(use gauche.process) (define (calc n) (dotimes (i n #f) (format #t "~a ~a~%" i (sqrt i)))) (define-syntax plot (syntax-rules () [(_ form) (with-output-to-process "gnuplot -p" (lambda () (format #t "plot '-' w l~%") form (format #t "end~%")))] )) (plot (calc 300))
slib
前回、突然登場したslibとは、何者か?
SLIB is a portable Scheme library that conforms to R^5.
R5RSの時代に、色々なschemeで共通に動くようにしたライブラリーの集合体。
vbox$ ls *.init RScheme.init gambit.init mitscheme.init scm.init vscm.init STk.init guile-2.init mzscheme.init scsh.init bigloo.init guile.init s7.init sisc.init chez.init jscheme.init scheme2c.init t3.init elk.init kawa.init scheme48.init umbscheme.init
どんなschemeで動くかは、上記で分かる。ここにchickenが居ないのはeggsと言う独自のパッケージングをしてるからかな? それから、gaucheが無いじゃんと嘆く必要は無い。shiroさんが独自にポーティングしてるから、普通に使える(あえて使う必要が無い程、gaucheは充実してるけどね)。
at gambit
gambitでもslibを使えるようにしたんで、実験(at OpenBSD)。
(load "/usr/local/share/slib/gambit.init") (require 'macro-by-example) (require 'format) (define (calc n) (for-each (lambda (i) (format #t "~a ~a~%" i (sqrt i))) (iota n 1))) (define-syntax plot (syntax-rules () [(_ form) (with-output-to-process (list path: "gnuplot" arguments: '("-p")) (lambda () (format #t "plot '-' w l~%") form (format #t "end~%")))] )) (plot (calc 300))
define-syntax
slibにはマクロ関係のライブラリィーが多数収録されている。下記は、 slib/READMEから抜き出してみたものだ。
`defmacex.scm' has defmacro:expand*. `mbe.scm' has "Macro by Example" define-syntax. `scmacro.scm' is a syntactic closure R4RS macro package. r4rsyn.scm, synclo.scm, synrul.scm have syntax definitions and support. `scmactst.scm' is code for testing SYNTACTIC CLOSURE macros. `scainit.scm' is a syntax-case R4RS macro package. scaglob.scm scamacr.scm scaoutp.scm scaexpp.scm have syntax definitions and support. `syncase.sh' is a shell script for producing the SLIB version from the original. `macwork.scm' is a "Macros that work" package. mwexpand.scm mwdenote.scm mwsynrul.scm have support. `macrotst.scm' is code from R4RS for testing macros.
普通に使うだけなら、上記のrequireだけで十分。その正体はmbe.scmだ。心して拝んでおけ。
let loop
ここで、schemeで多用される let loopについて復習。正式名称は、named letってやつ。
(let loop ((num 1) (sum 0)) (if (> num 100) sum (loop (+ num 1) (+ sum num))) ) ;; これは次のコードと等価。つまり再帰で計算をしている。 ;; 末尾再帰なので実行効率はループと同じ。 (let () (define (loop num sum) (if (> num 100) sum (loop (+ num 1) (+ sum num)))) (loop 1 0))
慣れると便利な書き方だ。
> (define (count-line) (let loop ((c (read-char)) (count 0)) (cond ((eof-object? c) count) ((char=? c #\newline) (loop (read-char) (+ count 1))) (else (loop (read-char) count))))) > (count-lines) a bb cc dd 4
stdinから入力したデータの行数をカウントする例。
(define (count-line) (do ((c (read-char) (read-char)) (count 0 (if (char=? c #\newline) (+ count 1) count))) ((eof-object? c) count) ))
同じ事をfor相当のdoで書いた例。何かわけわかめだな。
plot2 by gambit
さて、schemeからgnuplotを操る例が出て来たけど、1画で1本の折れ線しか引けなかった。 今度は、1画で2本の線を引きたい。
gnuplotの制約で、標準入力から2つのデータを与えられないんだ。どうしても、データをファイルに落として、それを使う事になる。
1 1.11 -1 2 2.22 -2.22 :
こんな具合に、最左はX値、中央は一本目のグラフのY値、その右側は二本目のグラフのY値って具合のファイルを用意する必要がある。
1本目の2本目も、データは X Y って単純な形式のデータにしたい。
sakae@pen:/tmp/t$ cat s1 1 1.11 2 2.22 sakae@pen:/tmp/t$ cat s2 1 -1 2 -2.22 sakae@pen:/tmp/t$ paste -d' ' s1 s2 | cut -d' ' -f1,2,4 1 1.11 -1 2 2.22 -2.22
shellで書くと、パイプの出力をファイルに落とした形にしたい訳だ。schemeで相当の事をしたい。って事で、捻り出したのが下記。
(load "/usr/share/slib/gambit.init") (require 'macro-by-example) (require 'format) ;; (string-split "ab cd" #\space) => ("ab" "cd") ; Gauche already has. (define (string-split str spliter) (let loop ([ls (string->list str)] [buf '()] [ret '()]) (if (pair? ls) (if (char=? (car ls) spliter) (loop (cdr ls) '() (cons (list->string (reverse buf)) ret)) (loop (cdr ls) (cons (car ls) buf) ret)) (reverse (cons (list->string (reverse buf)) ret))))) (define (calc n f) (apply string-append (map (lambda (i) (format #f "~a ~a~%" i (f i))) (iota n 1)))) (define (combo s1 s2) (let ([p1 (open-input-string s1)] [p2 (open-input-string s2)] [op (open-output-file "z_")]) (let loop ([l1 (read-line p1)] [l2 (read-line p2)]) (if (eof-object? l1) (begin (close-port op) (close-port p1) (close-port p2)) (begin (format op "~a ~a~%" l1 (cadr (string-split l2 #\space))) (loop (read-line p1) (read-line p2))))))) (define-syntax plot2 (syntax-rules () [(_ str1 str2) (begin (combo str1 str2) (with-output-to-process (list path: "gnuplot" arguments: '("-p")) (lambda () (format #t "plot 'z_' using 1:2 w l, 'z_' using 1:3 w l~%"))))])) ; (plot2 (calc 100 sqrt) (calc 100 log))
二つのcalcの結果を合成して、ファイル(z_)に落とす手続きがcomboだ。string-splitを別に用意して使っている。
gaucheはstring-splitを自前で用意してて、至れり尽くせりの環境だ。文字列の途中にscheme側の変数値を埋め込むなんてのも朝飯前。本当はこの機能を使って、データファイル名を、動的にしたかったんだけど、gambitだと煩雑になりそうなので、止めた。
calcの結果は、文字列で欲しい。一度mapを使って、一行毎の文字列リストを作成。それをapply string-append で、一気に、結合してる。applyを思い出すまで、不格好な方式で実現してたのは秘密だ。
plot2 by guile
今度は同じ事をguileでやってみる。まあ、互換性のチェックと言うか、インプリメントの独自性チェックと言った所かな。
(use-modules (ice-9 rdelim)) ;; read-line (use-modules (ice-9 popen)) (define* (iota count #:optional (start 0) (step 1)) (let lp ((n 0) (acc '())) (if (= n count) (reverse! acc) (lp (+ n 1) (cons (+ start (* n step)) acc))))) (define (calc n f) (apply string-append (map (lambda (i) (format #f "~a ~a~%" i (f i))) (iota n 1)))) (define (combo s1 s2) (let ([p1 (open-input-string s1)] [p2 (open-input-string s2)] [op (open-output-file "z_")]) (let loop ([l1 (read-line p1)] [l2 (read-line p2)]) (if (eof-object? l1) (begin (close-port op) (close-port p1) (close-port p2)) (begin (format op "~a ~a~%" l1 (cadr (string-split l2 #\space))) (loop (read-line p1) (read-line p2))))))) (define-syntax plot2 (syntax-rules () [(_ str1 str2) (begin (combo str1 str2) (let ((gp (open-output-pipe "gnuplot -p"))) (format gp "plot 'z_' using 1:2 w l, 'z_' using 1:3 w l~%") (close-port gp)))])) (plot2 (calc 100 sqrt) (calc 100 (lambda (x) (* 0.1 x))))
出来上がったものだ。string-splitは最初から提供されてた。これぐらいは必需品なので、ユーザーを煩わせないで欲しい。でも、iotaの挙動が。。。
ob$ guile GNU Guile 2.2.7 scheme@(guile-user)> (iota 3 1) ;;; <stdin>:1:0: warning: possibly wrong number of arguments to `iota' ice-9/boot-9.scm:1151:0: In procedure iota: Wrong number of arguments to #<procedure iota (n)> scheme@(guile-user)> (use-modules (srfi srfi-1)) scheme@(guile-user)> (iota 3 1) $1 = (1 2 3)
上記のようにオプション有りは受付無い状態だった。srfi-1のモジュールをロードすれば使えるようになる。今回はiotaだけを、さらって来て入れてみた。
次は、read-lineが無い。変な名前でモジュールになってた。それから、外部アプリのgnuplotとの交信方法。guile側の文字列をパイプに送り出すってんで、それ用のモジュールを入れて、事無きを得た。この他に pipeline なんて楽しいのも提供されてた。
schemeのまとめ
各種schemeを使って、外部アプリであるgnuplotでグラフを書かせる例題をやったけど、
gaucheは親切過ぎる。
guileは、まあまあ。
gambitは、使い勝手は悪いけど、コンパイルして爆速になるのが嬉しい。
って、結論かな。素直にgaucheを使うのが良いだろうけど、生憎OpenBSDでは動かない。爆速系には、chez schemeも有るけど、動くのはLinux系だけ。各種の手続きが用意されていないってのも痛い所だ。
grapheps
slibの作者様のお勧め品。どうやらschemeでpostscript語のデータってかスクリプトを発生させる物のようだ。どうやって使うのかな?
http://people.csail.mit.edu/jaffer/Docupage/grapheps#Other-Languages
scheme@(guile-user)> (use-modules (ice-9 slib)) scheme@(guile-user)> (require 'grapheps) ERROR: In procedure scm-error: slib:require unsupported feature grapheps
で思い出したのはSICP。
A Picture Language (from SICP 2.2.4)
https://sicp.iijlab.net/solution/ex2.2.html#zukei
図形描画 スクリプト(with Canvas and JavaScript)
OpenBSDにたまたま入っていた gs を使って、図を書いてみるかな。