これからのOCamlについての噂 #3: { foo = foo } => { foo } および labeled arguments の解説

ラベル付き引数について知っとる人は最終節まで飛ばしてもらって頂戴。

OCaml のラベル付き引数について

OCaml には labeled argument などという怪しからん物があって、半分くらい私責任があるのです。

寄り道

その辺りどういういきさつで OCaml の実装に入るようになったかは実は私と惹玖の間で認識の違いがあるのです。修論の時に私が OCaml を改造して labeled argument を実装していたら研究室のハードディスクがクラッシュした上、バックアップがなかったので、一度失われたらしいのですが、
私には

そんなこと全く記憶に無い

のです。いやホント。私の記憶では修論を書いていたらある日、惹玖がやってきて、「実装したよー」と言ってきた事しか覚えていない。嫌なことは選択的に忘れるらしいのです。その上、良いことさえ忘れる今日この頃ですが。

閑話休題

Labeled argument ってのは関数の引数を与えるときにラベルを付ける事で、引数順序を可換にしたり、引数のドキュメント性を高めたり、省略可能引数を使えたりできる機能です:

let print_char y x char =
  Curses.move y x;
  Curses.addch char
;;

はい、これはターミナルライブラリ Curses を使った(ことにしてある)与えられた座標に一文字表示する関数です。

で、いつも思うんですけど、なんで、Curses って y 軸(row) から指定するんでしょうね。二次元デカルト座標の表示といったら x 軸(column) から先に書くのがやっぱり自然だと思うんですけど。逆になっているのを忘れてしばしば row と column を逆に書いちゃうことって結構良くあるのです。row も column も型は int なので、逆に書いてしまっても型システムは間違いを指摘してくれません。そこで labeled arguments:

let print_char ~row:y ~column:x char =
  Curses.move ~row:y ~column:x;
  Curses.addch char

座標を指定する引数に ~row と ~column というラベルを付けてあります。Curses.move という関数も改造した(ことにして)やはり ~row と ~column というラベルを付けました。コンパイル時に -w L というスイッチを付けると、print_char 関数を使う際にはこのラベルを付けないと警告になります:

# print_char 10 20 'c';;
Warning L: labels were omitted in the application of this function.

さらに -warn-error L スイッチを付ければエラーになります。ユーザーは嫌でも

print_char ~row:10 ~column:20 'c'

と書かないとコンパイルできません。で、10がrowで、20がcolumnってのを意識して書かざるを得ませんから、x軸y軸をごっちゃにしてしまうということもなくなります。

Labeled arguments には上で見たような型システムでは押さえられない落とし穴をコメントではなくプログラム中にドキュメントとして持っておくことでカバーできる、という効用があります。

さらに引数がラベルで識別できるので、引数順を自由に出来ます:

print_char ~column:20 ~row:10 'c'

はい、x軸が先になりましたが大丈夫です。

寄り道

、、、と書いてきましたが、いまだに row, column どっちがどっちかすぐに忘れるのです。row が行。 column が列。なるほど、行と列、ってどっちがどっちかということですか。そいうや、中学の時にこう覚えたってのを例によって忘れていた:

  • 行は字中に=が入っているから、横に長い形状。1行、2行というとy軸成分を数えることになる。
  • 列は字中に||が入っているから、縦に長い。1列、2列でx軸成分を数える。

row と column はじゃあどおやって覚えるのか?横にいたアメリカ人とオーストラリア人に聞いたが、あまり納得できる覚え方は教えてくれない。「ギリシャ建築の柱のことを column って言うでしょ、だから、column は縦に長いんだよ。」いや、そういうんじゃ駄目でしょ。アメリカやオーストラリアじゃないんだから、日本の街中にオリンポス宮殿なんか無いんですよ。
とか愚痴っていたら、column は l という縦長成分があるから縦長の方だ、row は全部背が低い文字だから横長の方、とそういや中学のころ覚えたってのを忘れていた。

と、言うわけなんですが、、、なぜか OCaml では -w L, -warn-error L はデフォルトでは off になっているので、ラベル付き関数にラベルなし引数でも、つまり、print_char 20 10 'c' でも通っちゃうのです。これが結構混乱の元なんで、是非デフォルトでエラーにして頂きたいです。皆さんも Makefile には -w L -warn-error L を忘れないように!!

略記法 ~label:label => ~label

で、row が y で、 column が x だってのはもう常識だよね?じゃあ、row は row、 column は column って書くほうがいいに決まってるよね、常識だし!

let print_char ~row:row ~column:column char =
  Curses.move ~row:row ~column:column;
  Curses.addch char

うんうん、綺麗になりました。でも、~row:row とか ~column:column とか書くのって何だか面倒だよね。二回同じ事書くわけだし。実は省略できますよ:

let print_char ~row ~column char =
  Curses.move ~row ~column;
  Curses.addch char

これは上と全く同じコードです。

この ~label:label を ~label と略記する方法はなかなか便利なのです。次のようなコードって結構書くことがあります:

let row = compute_row ..なんか長い.. in
let column = compute_column ..長いからlet.. in
Curses.move row column

引数に長い式をダラダラ書くくらいだったら let で変数にバインドしてから使うわけだけど、a, b なんて変数名じゃ馬鹿みたいだから、引数の意味を表す row, column という変数を使いますね。ここで、labeled arguments を使うと、Curses.move ~row:row ~column:column な訳だけど、略記法を使えば、Curses.move ~row ~column と書ける。逆に言えば、略記法を使うことで、一時変数名にも意味のある名前を使うように強制することが出来るようになります。

でもこの略記法を使うと、ラベル付き引数の事知らない人が見ると訳わかんなくなるよね。なんだか、引数の前に鬚(~)がついてるんだけど、これ何だろう?(-warn L が無いから)鬚つけなくても付けても動くけど?というわけでやっぱりこれはデフォルトでonにしてほしいのです。

Optional arguments

Labeled arguments の更なる機能に省略できるラベル付き変数ってのがあるんだけど、これは今回のトピックと直接関係ないから、また今度。

本題。{ foo = foo } => { foo }

で、やっと本題。将来入るかもしれない OCaml の機能の話です。

Curry化ってご存知ですよね。Tuple 引数適用を複数の引数適用に変換する、アレです。OCaml やってたら、まぁまずどの入門書にも初めのほうの章に鬼の首を取ったかのように嬉しそうに書いてあるあれですよ。上の例だと、こんな感じかな?

let print_char_uncurried (row, column) char =
  Curses.move row column;
  Curses.addch char

let print_char row column char =
  Curses.move row column;
  Curses.addch char

これと同じことをラベル付き引数の世界で考えるとこうなる:

(* type t = { row : int; column : int } *)
let print_char_uncurried { row = row; column = column } char =
  Curses.move ~row ~column;
  Curses.addch char

let print_char ~row:row ~column:column char =
  Curses.move ~row ~column;
  Curses.addch char

ははぁ、レコード適用をCurry化するとラベル付き引数列になるわけだ。

じゃあ、~row:row => ~row としていいんだから、このルールをレコードに適用して悪いはずが無いということになりますね:

let print_char { row; column } char =
  Curses.move ~row ~column;
  Curses.addch char

let _ = 
  let row = compute_row .... in
  let column = compute_column .... in
  print_char_uncurried { row; column }

うーんなるほど。

で、これが将来のOCamlに入るって話を聞いたってのを忘れていたのを思い出した。しかしこれもまた初心者泣かせな機能ではあるよね。