Date2011年11月29日

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

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

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

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

let sqr_sum l =
  List.fold_left (+) 0
    (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 (+) 0

うん、すっきりしました。
なにより、「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 (+) 0

見てもらえば分かる通り、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でこれらの演算子を使うのはあまりよろしくないかもしれません。
ものすごく簡潔になる場合だけ、コメントで注釈を添えて使うくらいかな……

O’Reilly他のセール情報まとめ

昨日、O’Reillyの電子書籍がAndroidで買うと安いという記事を書きましたが、残念ながら自分の欲しいProgramming F#はありませんでした。
で、諦めきれずにいろいろ調べてみたところ、どうやらO’Reilly.comはいろいろとセールをやっているみたいです。

まず、書籍を2冊買うと1冊タダになるキャンペーン
電子書籍などでも適用されるようです。
んじゃ、安い書籍を2冊買って高い書籍1冊をタダにすればお得じゃね? という発想が当然出てきますが、残念ながら仕組みとしては3冊以上購入時に購入額の1/3を割り引く模様。
そりゃそうですよね…… 

次に、10 Weeks of Gift Ideals
こちらは10週連続で割引書籍(電子書籍は50%off、プリントは40%off)が追加されていくもの。
現在3週目までオープンされていて、Java Script、HTML5、Makeが半額となっています。
ちなみに、紹介している割引はすべてクーポンコードを入力する(あるいは自動入力される)仕組みで、クーポンコードは一度に一つしか適用できないため重ねがけはできないみたいです。

で、上記2つの割引を全部ぶち壊しにするセールがExclusive Membership Offers。
こちらはアカウントを作ってサインインしないといけませんが、なんと1冊買うと1冊無料、2冊買うと2冊無料、3冊買うと三冊無料だそう。
……要するに半額ですね。というか、2冊以上なら常にクーポンコードが有効みたいです。
少なくとも、2冊で1冊無料のセールとは上位互換、10週連続セールも2冊以上買うなら書籍の制限なく使えます。
アカウント登録するだけでこれなら、わざわざ他のセールで買う必要あまりないような……

最後にもう一つ、Ciber Mondayは対象書籍が60%offになります。
これは下位互換セールじゃないですね。
対象書籍が限られているのと、本日29日までのセールなので、欲しい本がある方はお早めにどうぞ。

O’Reillyだけでも十分セールだらけですが、あちらのBlack Friday(感謝祭明けの金曜日)セールなのか、そこらじゅうで色んなセールがあるようなのでそちらもご紹介。

まずはManning Publications Co.Green Tuesdayセールを実施中。
「電子書籍は紙を使わないから環境にいいよ」というセールらしいです。
閲覧の度に電力使うこと考えたら、本当に環境にいいのか怪しい気がしますが……
ともあれ、50$未満の書籍40%off、50$以上の書籍は50%offだそうです。

次にApressもHappy Ciber Mondayセールを実施中。
こちらは、すべての電子書籍が$15というもの。
あまりApressの書籍は詳しくないですが、物によってはかなり高い書籍もあるようなのでお得なんじゃないでしょうか?

上記2つとO’ReillyのCiber Mondayは、ちょうど今日まで(向こうの時間なので時差はあるが)なので、気になる方はお早めにどうぞ。
自分は、結局F#書籍が期限セールじゃないのでもうちょい見守りますかね。

そうそう、O’Reillyは電子書籍がマルチフォーマットなのですが、その中にapk形式というのが含まれている物があります。
これはAndroidアプリ形式なので、おそらくこのapk形式を含んでるものがAndroidマーケットでも購入できるものなんじゃないかと。確かめてませんが。
Androidマーケットでいちいち調べるのは面倒なので、ここで調べてapk含んでいるものはAndroidマーケットで買う、というスタイルが一番ラクな気がします。
ちなみに、apkからはepubが書き出せるので、実質PCから買う利点はPDFやmobiに価値を感じるかどうか。
mobiはKindleを持っていないとあまり意味がないので、PCから見たいなら実質PDFに価値を感じるかですね。
epubは組版情報を持っておらず、図の多い書籍などはレイアウトが崩れて読みにくい可能性が高いので、図の多そうな書籍や図の位置が重要そうな書籍は多少割高ですがネットで買った方がいいんじゃないかと思います。
……ま、買う前に図の位置が重要そうかわかればの話ですが。

【追記】
日本語訳出ている書籍ばかり調べていたので見逃していましたが、ApressにもF#関連の書籍はあるようです。
というかAmazon.comで見ると結構評判良さそうですね……
$15、今なら日本円で1,100円くらいですか…… 安いけど衝動買いしてしまっていいか迷う……