srfi-42の紹介(loopマクロ:scheme版のような)

はじめに

多くの言語はちょっとしたループを回すような処理を手軽に記述する方法を持っています。CLにはloopマクロ*1がありますし、pythonhaskellなどはリスト内包表記などを持っています。ちなみにschemeにもそんな方法があります。srfi-42で定義されている"Eager Comprehensions"を使います。名前の通り、haskellのそれとは異なりeagerに(正格に)評価します。

たまたま、arielのlisp脳の勉強会?のustでloopマクロに言及した箇所があったので、scheme版のloopマクロ*2とでも言えるsrfi-42の利用例をちょっとだけ紹介してみます。

以降はschemeの処理系としてgaucheを利用しています。srfi(Scheme Requests for Implementation)は言語仕様(RnRS)の外にはありますが、多くの処理系が対応していることから、schemeの標準ライブラリと考えて良いです。なので、他の処理系でもライブラリの読み込み部分以外は同様の記述で動くはずです。

使い方

利用するには、srfi-42を読み込んでください。

(use srfi-42)

それでは、利用例を紹介します。

利用例

srfi-42のライブラリを読み込むとfoo-ecというマクロがつかえるようになります。(foo=list,sum,append...)ここでは、一番わかりやすいlist-ecを使うことにします。

1~9までのlistを作る。

だいたい(list-ec )というような形で利用します。1~9までのリストを生成するには以下のように書きます。pythonのrangeと同様に10以下を表します。

(list-ec (: i 10) i) ; => (0 1 2 3 4 5 6 7 8 9)

ループの初期値も設定できます(defaultは0)。

(list-ec (: i 5 10) i) ; => (5 6 7 8 9)

もちろん、増減幅も設定できます(defaultは1)。

(list-ec (: i 1 10 3) i) ; => (1 4 7)
二乗した値のlistを作る
(list-ec (: i 10) (* i i)) ; => (0 1 4 9 16 25 36 49 64 81)
条件による絞り込み(奇数のみ集める)

リスト内包表記なので条件式を渡せます。省略していましたが、実際のところは(list-ec )という形です。が条件式です。

(list-ec (: i 10) (if (odd? i)) i) ; => (1 3 5 7 9)
(list-ec (: i 10) (not (even? i)) i) ; => (1 3 5 7 9)
途中式を一時変数に格納(:letの利用)

普通のS式のように途中の値を変数に束縛することも可能です。以下は二乗した結果が偶数なもののみを集めたリストです。

(list-ec (: i 10) (:let sq (* i i)) (if (even? sq)) sq)
 ; => (0 4 16 36 64)
組のリストの作成(複数のを利用)

の引数にリストを渡すと、listを順に走査してくれます。このことと、後述する複数のの利用によって、組のリストの作成が手軽に記述できます。

(list-ec (: e '(a b c d)) (list e)) ; => ((a) (b) (c) (d))

複数のを渡すことができます。実際のところfoo-ecを呼ぶ際の形式は、(list-ec ... )です。複数のを渡した場合にはループをN重に回すのと同様の結果が得られます。

(list-ec (: i '(1 2 3)) (: j '(a b c)) (cons i j))
 ; => ((1 . a) (1 . b) (1 . c) (2 . a) (2 . b) (2 . c) (3 . a) (3 . b) (3 . c))

map,for-each,foldと同様に並行的に走査して欲しい場合には:pararelでwrapします。

(list-ec (:parallel (: i '(1 2 3)) (: j '(a b c))) (cons i j))
 ; => ((1 . a) (2 . b) (3 . c))

外側の変数を中のループで使うことも可能です。

(list-ec (: i 3) (: j i 3) (cons i j))
 ; => ((0 . 0) (0 . 1) (0 . 2) (1 . 1) (1 . 2) (2 . 2))

その他foo-ec

(do-ec (: i 10 100 10) (print i))

(list-ec (: i '((a b c) (d e f))) i) ; => ((a b c) (d e f))
(append-ec (: i '((a b c) (d e f))) i) ; => (a b c d e f)

(vector-ec (: i 10 0 -1) (if (even? i)) i) ; => #(10 8 6 4 2)
(vector-of-length-ec 10 (: i 10) i) ; => #(0 1 2 3 4 5 6 7 8 9)

(sum-ec (: i 11) i) ; => 55
(fold-ec 0 (: i 11) i (lambda (i n) (+ i n))) ; => 55
(product-ec (: i 1 11) i) ; => 3628800
(fold-ec 1 (: i 1 11) i (lambda (i n) (* i n))) ; => 3628800

(min-ec (: i '(10 20 -30 40 50)) i) ; => -30
(max-ec (: i '(10 20 -30 40 50)) i) ; => 50

(every?-ec (: i '(1 2 3 4)) (positive? i)) ; => #t
(every?-ec (: i '(1 2 3 -4)) (positive? i)) ; => #f
(any?-ec (: i '(1 2 3 4)) (positive? i)) ; => #t
(any?-ec (: i '(1 2 3 -4)) (positive? i)) ; => #t

(first-ec #f (: i '(1 2 -3 4 -5)) (if (negative? i)) i) ; => -3
(first-ec #f (: i '(1 2 3 4 5)) (if (negative? i)) i) ; => #f
(last-ec 'foo (: i '(1 2 -3 4 -5)) (if (negative? i)) i) ; => -5
(last-ec 'foo (: i '(1 2 3 4 5)) (if (negative? i)) i) ; => foo

vector-ecよりはvector-of-length-ecの方が高速です。ただし、事前に生成されるvectorのサイズを指定しなければなりません。

活用事例

組み合わせの計算などが簡単に記述できます。

(use srfi-1)
(use srfi-42)

(define (combinations xs n)
  (cond ((= n 1) (map list xs))
        (else
         (list-ec (: i xs) (: es (combinations (delete i xs) (- n 1)))
                  (cons i es)))))

(combinations '(a b c) 2)
 ; => ((a b) (a c) (b a) (b c) (c a) (c b))


でもcall/ccを使わなければ途中で止められません。

*1:それ以外にもseriesとかiterateとかあるみたいです

*2:srfi-42がscheme版のloopマクロという表現は適切ではないかもしれません。ちなみに、chickenというscheme処理系のライブラリの中には、"loopy-loop"というCLのloopのような記述ができるようになるものがあったりします。