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は充実してるけどね)。

http://people.csail.mit.edu/jaffer/SLIB.html

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

PostScript入門

で思い出したのはSICP。

A Picture Language (from SICP 2.2.4)

https://sicp.iijlab.net/solution/ex2.2.html#zukei

図形描画 スクリプト(with Canvas and JavaScript)

OpenBSDにたまたま入っていた gs を使って、図を書いてみるかな。

Programming in Schelog

https://github.com/ds26gte/schelog

各種schemeで動くprologだそうです。各種をどうやって吸収してるか、興味は尽きないな。


This year's Index

Home