F#のパイプライン演算子・関数合成をOCamlで実装

最近F#が気になっているのだけれど、F#で魅力を感じる機能の一つに、パイプライン演算子というのがあります。
これはUnixのパイプライン処理に類似するもので、値をある処理に与えて、その結果をまた別の処理に与えて……というのをかなり見やすく記述できる、というものです。
例えば、リストの各要素を2乗してから和を取る、という関数sqr_sumは、素直に書くと次の様になります。

let sqr_sum l = List.fold_left (+)  (List.map (fun x -> x * x) l)

まぁ読めなくはないですが、どこから処理されるのか一瞬迷いますよね。
もっと複雑に、リストのうち奇数であるような要素のみを取り出し、これを2乗して和を取る、なんてのを書いてみましょう。

let sqr_sum l =
  List.fold_left (+) 
    (List.map (fun x ->; x * x)
      (List.filter (fun x ->; x mod 2 = 1) l ))

……うん、読めない。
いや、慣れてれば読めるのかもしれませんが、少なくとも自分はこれを見て「なるほど、filterしてからmapしてfoldするんだな」とは思えません。
しかも、同じようにfoldとかを増やしていくと、どんどんインデントが下がっていきます。
それは困る。

というわけで、こんな時はパイプライン演算子の出番です。
同じ関数をパイプライン演算子を使って書くと、次のようになります。

let sqr_sum l =
  l |> List.filter (fun x -> x mod 2 = 1)
    |> List.map (fun x -> x * x)
    |> List.fold_left (+) 

うん、すっきりしました。
なにより、「filterしてmapしてfoldする」という処理順がわかりやすくなります。いいですね。
……多分、「let使えよ」とか「そもそもfold一個でやったほうが効率がいい」というツッコミをしたい方がいるかと思いますが、それは例が悪いということで。

さて、このパイプライン演算子、OCamlには存在しないのですが、わりと簡単に似たような機能を実装することができます。

let (|>) x f = f x
let (<|) f x = f x

演算子の結合順とか細かい所で違うかもしれませんが、少なくとも使ってみた感じは十分実用できそうです。

次に、関数合成について。
上の例では、lを渡していく、というニュアンスで書いていますが、lに次々関数をかけていく、というのは、関数合成と見ることも出来ます。
F#では関数合成演算子も用意されているので、先ほどの関数は以下のように書き換えることも出来ます。

let sqr_sum =
  List.filter (fun x -> x mod 2 = 1)
  >> List.map (fun x -> x * x)
  >> List.fold_left (+) 

見てもらえば分かる通り、lはどこにも出て来ません。
これは、先ほどのパイプライン演算子を使った例ではlが冗長な情報だったわけですね。
「let f x = h (g x)」みたいな関数なら、そもそもxという情報を省いて「f = g >> h」でいいわけです。
これは「f=g○h」と読めば、数学的記法とも対応が取れています。
ちなみに流儀によって「g○h(x)=h(g(x))」とする場合と、「h○g(x)=h(g(x))」とする場合があるようですが、これらはそれぞれ「g >> h」と「g << h」の二種類の関数合成演算子が用意されているので、どちらのスタイルでも書くことができます。見慣れると、適用順が見やすいのでF#スタイルの方がわかりやすいかも。で、関数合成もOCamlにはないんですが、これまた簡単に似たようなものを実装できます。

let (>>) f g x = g (f x)
let (<<) f g x = f (g x)

ちなみに、F#でどうなってるかは確認していませんが、少なくともこの方法でOCamlに実装した場合は、上位になる側の関数が引数をいくつ取るものでも合成できます。
例えば、リストの要素を2乗したものの集合isは次のように構成できます。

(* int型の要素を持つ集合 *)
module IntSet = Set.Make(struct type t = int let compare = compare end)
(* 整数の2乗 *)
let sqr x = x * x
(* リストの要素を2乗したものの集合 *)
let is = List.fold_right (sqr >> IntSet.add) [1;2;3] IntSet.empty

これはかなりすっきりしている気がしますね。

ただしパイプライン演算子や関数合成が万能というわけではなく、引数の順序によってはうまく適用できない場合があります。
例えば、上の「リストの要素を2乗したものの集合」を、リストを固定せず、リストを引数とする関数sqr_setにしてみましょう。

let sqr_set l =
  List.fold_right (sqr >> IntSet.add) l IntSet.empty

この時、lというのは冗長な情報なので省きたいところですが、foldに対してlの適用を明示しないと、IntSet.emptyが適用できないため省略できなくなってしまいます。
それじゃあ代わりにList.fold_leftを使えば、と思うのですが、今度はIntSet.addとの引数順の対応が取れないため、ラムダ式を作らないといけなくなります。

let sqr_set =
  List.fold_left
    (fun making x -> IntSet.add (sqr x) making)
      IntSet.empty

余計すっきりしなくなりました。
ちなみにこれはパイプを使って、

let sqr_set =
  List.fold_left
    (fun making x -> sqr x |> IntSet.add <| making) IntSet.empty

と書くと、データの流れはわかりやすくなります。が、結局余計読みづらくなります。

この解決案の1つとして、引数の適用順を入れ替える関数switchを使う方法が考えられます。

let switch f x y = f y x
let sqr_set =
  List.fold_left (switch (sqr >> IntSet.add)) IntSet.empty

あるいは、fold_rightもswitchで書き換えることができます。

let sqr_set =
  switch (List.fold_right (sqr >> IntSet.add)) IntSet.empty

括弧が多くて、ちょっとデータの流れがわかりづらいですね。
ここはパイプライン処理を使って簡潔にしてみましょう。

let sqr_set l =
  List.fold_right (sqr >> IntSet.add)
  |> switch <| IntSet.empty

あれ、余計読みづらくなったか……
とりあえず、switchを使えばうまく行きそうです。

しかしながら上記の方法は、switchの挙動が直感的にわかりづらい、という致命的な欠点があります。
ぶっちゃけ余計読みづらくなってる気がします。
こんなとき、Scalaにあるアンダーバーを用いた匿名関数があればとても綺麗にかけます。
次のコードは「もしF#にアンダーバー記法があったら」というコードです。
実際には動かないので注意して下さい。

let sqr_set =
  List.fold_right (sqr >> IntSet.add) _ IntSet.empty

端的に言うと、Scalaのアンダーバー記法は「f _」というのが「fun x -> f x」と等価です。
自分もちゃんと勉強したわけではないので間違っているかもしれませんが、要は「わざわざ名前つける必要ないんだから匿名変数にしようぜ」というアイディアみたいです。
これ、めちゃくちゃ便利そうですよね。
惜しむらくは、OCamlどころかF#にもこんな記法はないっぽい上、文法レベルの問題なので似たようなことを関数で書くことができない、というところでしょう。
F#にも導入してくれないかなぁ……

というわけで最後はパイプライン処理の話じゃなくなってしまいましたが、パイプや関数合成が便利だよ、という話でした。
問題はOCamlでこれらの記法を用いてプログラムを書くと、他の人が読むのが大変になる、というところですね……
どれだけ簡潔に書けても、読む人がこれらの記法を知らなければ余計に混乱するわけで、そう考えるとOCamlでこれらの演算子を使うのはあまりよろしくないかもしれません。
ものすごく簡潔になる場合だけ、コメントで注釈を添えて使うくらいかな……