Schemeの紹介と内包表記マクロ

LispとS式

SchemeCommon LispのようなLisp系言語の強みは強力なマクロ機能にある。LispではプログラムはS式と呼ばれる形式で記述され、それは、Lispがデータとして扱うリストと全く同じ構造を持っている。そのため、LispがデータとしてLispのプログラムを簡単に処理できるのだ。マクロを使うと、特定の問題に特化した、新しい言語とさえ呼べるような機能 (DSL) を簡単に作成して使うことができる。

Schemeのプログラムは基本的にはすべて以下のような構造を持っている。これ自体がS式であり、関数や引数の部分にはまたS式を入れることができる。関数の呼び出しでは引数は先に評価されて関数に渡されるが、マクロでは評価前の式がそのまま渡される。

(関数 引数 引数 ...)
(+ (apply * (iota 10 1)) (square *e*)) ; プログラムの例
1 ; 括弧なしの1つの式もS式

Schemeとマクロ

Schemeにはマクロ展開時の変数名の衝突を自動的に回避してくれる健全なマクロが用意されている。(意識的に衝突を起こしたり、展開時の環境に変数を束縛したりしたい場合は、処理系によっては、explicit renamingによるマクロやdefine-macroによるマクロなども利用できたりする。) syntax-rulesは独自のパターンマッチ言語を使ってコードを置き換える感じなので、簡単だがScheme自体でリストを操作していないという不満もあるらしい。R7RS-largeで入るとうわさされているexplicit renamingによるより低レベルなマクロではリストをより直接操作できる。

最近の言語では大抵用意されていたりするリストの内包表記。SchemeでもSRFI 42に用意されているが、なんとなく馴染む構文が欲しかったので実装してみた。また、Haskellのように[1..10]みたいにリスト生成したり、Fortranのようにa(2:5)でリストのスライスが欲しかったりしたのでこれも書いてみた。

(import (scheme base) (srfi 1))

; リスト生成マクロ
(define-syntax l:
  (syntax-rules (..)
    ((_ e) (l: 0 .. (- e 1)))
    ((_ s .. e) (if (> e s) (l: s (+ s 1) .. e) (l: s (- s 1) .. e)))
    ((_ s t .. e) (let ((diff (- t s)) (cp (if (> t s) > <)))
                    (let loop ((l `(,s)) (c t))
                      (if (cp c e) (reverse l) (loop (cons c l) (+ c diff))))))))

; リストのスライスを取るマクロ
(define-syntax l/s
  (syntax-rules (:)
    ((_ l s : e) (l/s (l/s l : e) s :))
    ((_ l s :) (drop l s))
    ((_ l : e) (take l (+ e 1)))
    ((_ l i) (list-ref l i))))

; 内包表記マクロ
(define-syntax l/c
  (syntax-rules (: <- .. :=)
    ((_ lm : (x <- xs) lc ...) (apply append (map (lambda (x) (l/c lm : lc ...)) xs)))
    ((_ lm : (x <- s .. e) lc ...) (l/c lm : (x <- (l: s .. e)) lc ...))
    ((_ lm : (x <- s t .. e) lc ...) (l/c lm : (x <- (l: s t .. e)) lc ...))
    ((_ lm : (d := ds) lc ...) (let ((d ds)) (l/c lm : lc ...)))
    ((_ lm : c lc ...) (if (not c) '() (l/c lm : lc ...)))
    ((_ lm :) `(,lm))))

実行例。

; 直角3角形を作る10以下の自然数の組とその面積
(l/c (list a b c area) : (a <- 1 .. 10) (b <- 1 .. a) (c <- 1 .. b)
                         (= (* a a) (+ (* b b) (* c c)))
                         (area := (/ (* b c) 2)))
=> ((5 4 3 6) (10 8 6 24))
; リストの生成
(l: 10) => (0 1 2 3 4 5 6 7 8 9)
(l: 1 .. 3) => (1 2 3)
(l: 2 4 .. 10) => (2 4 6 8 10)
; スライス
(l/s '(1 2 3 4 5) 1 : 3) => (2 3 4)
(l/s '(2 4 6 8 10) 2 :) => (6 8 10)

引数の評価については考えていないが、副作用を使わなければ大丈夫なはず。とりあえず動くので良しとしよう。こんな風に簡単に言語機能自体を拡張できるのを見るとやっぱりLispは楽しいと思う。

SchemeCommon Lisp

SchemeCommon Lispは現在でも広く利用されているLispの2大方言で、それぞれにたくさんの実装が存在する。もしかするとLispというとインタプリタのイメージが強いかもしれないが、コンパイラの実装もたくさんある。SBCLなどはコンパイルすればC並みの速度が出るのではないだろうか。Common Lispは使える機能は最初からとりあえず全て用意した大きな仕様で、Schemeは逆に(そこから全てを構成できる)より小さな機能を用意している気がする。ただ、Schemeは最初から継続が使える。また、Common Lispでは変数と関数の名前空間が分かれているのに対してSchemeではそれが同一なのも大きな違いだ。Schemeは変数と関数を同じように扱えるので関数の定義や高階関数も変数と同様に扱える。

(define *e* 2.718281828459045)
(define square (lambda (x) (* x x)))
(define (apply-f f x) (f x)) ; 関数定義用の構文も用意されているが、意味は無名関数の定義と束縛と同じ
(apply-f square *e*) => 7.3890560989306495

R7RSとSRFI

Schemeの最新の規格はR7RSで、R7RS-smallが2014年に策定され、R7RS-largeの策定作業が2017年現在も進められている。また、SchemeにはSRFIと呼ばれる標準ライブラリの規格があり、処理系が対応していれば利用できる。R7RSに対応した処理系は色々あるが、個人的におすすめなのは以下の3つ。

  • Gauche : 実用的な独自の拡張機能が充実している。
  • Sagittarius : SRFIへの対応度が高い気がする。
  • Chibi-Scheme : R7RSの策定グループの1人が作った参照実装なので、仕様を確認したい時に使える気がする。ただ深い再帰で止まってしまう。