SchemeでF# 風パイプライン演算子を書いてみた
パイプライン処理のこと
巷ではStreemやStreeem、さらにはstleemなんてものまで現れて、パイプライン風プログラミング言語が話題になっています。 パイプライン処理風のプログラミングスタイル、面白そうですね。
パイプライン演算子|>
は、F# 言語の標準的な記法の中で、パイプライン処理風の記法を実現する2項演算子です。詳しくはこのあたりにありますが、簡単な例を挙げると
[1..100] |> List.map (fun x -> match (x%3, x%5) with | (0, 0) -> "FizzBuzz" | (0, _) -> "Fizz" | (_, 0) -> "Buzz" | (_, _) -> string x) |> List.iter (printfn "%s")
このように、FizzBuzzもきれいに書けます。
また、パイプライン処理といえば並列実行ですね。F# では非同期ワークフローのページにあるような形ですね。async{ }
で単一要素に対する処理を組みあげて、要素のリストに対して適用する例が載っています(ページ最下部)。
本来なら、ここで並列実行されるプログラムを扱いたいところですが、大変そうなので今回はそこまでは扱いません。
今回の話
前置きが長くなりました。
今回は、私が最近勉強中のSchemeで、パイプライン演算子風の関数合成演算子を書いてみた話です。 動作環境はGauche 0.9.4です。
実装
関数2つを合成する
>>
は関数合成、*>
はパイプライン(|>
)となっています。
(define >> (lambda (f g) (lambda (x) (g (f x))))) (define *> (lambda (x f g) ((>> f g) x)))
マクロはまだよく理解していないため、関数による実装としました。
FizzBuzzってみる
まず、パイプラインにしないバージョン。(参考:「Lisp脳」の謎に迫る - Schemeプログラマの発想 - karetta.jp)
(for-each print (map (lambda (x) (cond ((= (modulo x 15) 0) "FizzBuzz") ((= (modulo x 5) 0) "Buzz") ((= (modulo x 3) 0) "Fizz") (else x))) (iota 100 1)))
読むときは後ろ側から、iota, map, for-eachと読むことになります。
次にパイプラインにするバージョン
(*> (iota 100 1) (map$ (lambda (x) (cond ((= (modulo x 15) 0) "FizzBuzz") ((= (modulo x 5) 0) "Buzz") ((= (modulo x 3) 0) "Fizz") (else x)))) (for-each$ print))
map$
とfor-each$
は、部分適用されるバージョンの関数です(参考: Gauche ユーザリファレンス: 6.18 手続きと継続)
だいぶパイプラインらしくなってきました。
しかしこのままでは、(*> x f g)
の形式、つまりF# で言うところのx |> f |> g
の形式にしか対応していません。
関数をいくつも繋げられるようにしてみる
(define >> (lambda fs (letrec ((>> (lambda (fs) (cond ((null? (cdr fs)) (car fs)) (else (lambda (x) ((>> (cdr fs)) ((car fs) x)))))))) (>> fs)))) (define *> (lambda (x . fs) ((call-with-values (lambda () (apply values fs)) >>) x)))
これで、x |> f |> g |> h |> ...
が(*> x f g h ...)
といった形式で書けるようになります。実装は、可変長引数にする都合でちょっと混乱しましたが、多分大丈夫です。
使ってみます。
(*> (iota 100 1); 1~100の整数 (map$ (lambda (x) (cond ((= (modulo x 15) 0) (list x "FizzBuzz")) ((= (modulo x 5) 0) (list x "Buzz")) ((= (modulo x 3) 0) (list x "Fizz")) (else (list x))))); 数とFizzBuzzのペア(リスト)に変換 (filter$ (>> car even?)); 数が偶数のもののみ取得 (for-each$ print)); 標準出力
例の内容自体はどうでもいいものですが、出力は以下のようになります。
(2) (4) (6 Fizz) (8) (10 Buzz) (12 Fizz) (14) (16) (18 Fizz) (20 Buzz) (22) (24 Fizz) (26) (28) (30 FizzBuzz) (32) ...(後略)
期待した通りに動いていそうです。
ということで今回はここまでです。
余談
Gauche ユーザリファレンス: 4.3 手続きを作る には$
マクロがあって、($ h $ g $ f x)
と書けば、今回作った(*> x f g h)
と同じように機能するようです。(順番は異なりますが、カッコの入れ子は削減できます。)
追記
Gauche ユーザリファレンス: 6.18 手続きと継続 にcompose
手続きがあり、それが今回の>>
と実質同じ意図の関数であったようです。見逃していました。*2
また、2引数以上の関数についての考慮を忘れていたようです。この記事の>>
手続きの定義は、(lambda (x) ...)
の箇所を、マニュアルのcompose
関数と同様に、call-with-values
とapply
を使うようにする必要がありそうです。
ということで書き換えてみた結果がこちらです。
(define >> (lambda fs (letrec ((>> (lambda (fs) (cond ((null? (cdr fs)) (car fs)) (else (lambda x (call-with-values (lambda () (apply (car fs) x)) (>> (cdr fs))))))))) (>> fs)))) ;; *>はそのまま
Gaucheのcompose
の実装を見たほうが良い気もしましたが、とりあえずこんな感じに。
動作確認。
(*> '(1 2 3) (map$ x->string); 数値→文字列 (cut apply values <>); リスト→多値 string-append; 文字列で結合 ) ;; "123"
多値をパイプライン中で使う上手い例は思いつきませんでした。