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 を使って、図を書いてみるかな。