技術memo

関数型ゴースト

SchemeでF# 風パイプライン演算子を書いてみた

パイプライン処理のこと

巷ではStreemStreeem、さらにはstleemなんてものまで現れて、パイプラインプログラミング言語が話題になっています。 パイプライン処理風のプログラミングスタイル、面白そうですね。

ところで、パイプラインといえばF# ですね? *1

パイプライン演算子|>は、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-valuesapplyを使うようにする必要がありそうです。

ということで書き換えてみた結果がこちらです。

(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))))
;; *>はそのまま

Gauchecomposeの実装を見たほうが良い気もしましたが、とりあえずこんな感じに。

動作確認。

(*>
 '(1 2 3)
 (map$ x->string); 数値→文字列
 (cut apply values <>); リスト→多値
 string-append; 文字列で結合
 )
;; "123"

多値をパイプライン中で使う上手い例は思いつきませんでした。

*1:要出典

*2:但し、compose手続きは>>関数と記述順が逆順のようです。(>> f g h) = (compose h g f)といった風に。