Source

ocaml-zippy-tutorial-in-japanese / random_memo.rst

Random memo

OCamlYacc オワコン。Menhir を使え

http://caml.inria.fr/mantis/view.php?id=1703

Since menhir is a much better alternative to ocamlyacc, we now recommend that all users switch to menhir. At any rate, we do not spend any time on new features for ocamlyacc.

トップレベルで let _ = は使わない

トップレベルで関数を部分適用して、そのまま何もしないのは、大抵の場合引数が足りず間違っているコード:

let _ = List.iter f          (* あれー、何も起こらないぞ? *)

このようなコードを let _ = で受けてしまうとコンパイラは何も言わずにコードを通してしまう(まあそらそうだ)。 トップレベルでは let _ = ではなく let () = と書くべき:

let () = List.iter f          (* 型エラーになる *)

let を伴わないトップレベルの関数呼び出しも同じ理由で推奨しない:

List.iter f;;                 (* あれー、何も起こらないぞ? *)

なお、 sequence で部分適用の式を書いてしまった場合は警告が出る:

List.iter f; prerr_endline "hello world"      (* Warning 5: this function application is partial *)

当然だが Warning 5 はエラー扱いにすべき。

本当に本当に本当に式の結果を無視したい時は ignore を使う:

ignore (List.iter f)          (* ほんとに意味はないけど俺はわかってやっているんだ!! *)

fun x -> match x with は function にしろ

let f x = match x with でも同様。まあ同じといえば同じだけれども文化的に function 推奨。

Constructor を curry 化したい

できない

なぜ OCaml のコンストラクタは partial application できないのか https://groups.google.com/forum/?fromgroups=#!topic/fa.caml/IOlkCcVBg5Q

CamlP4 で各コンストラクタに対応する curried form な関数を定義することはできる。 http://www.digipedia.pl/usenet/thread/14273/5152/ 現在も動作するかは不明。 type-conv を使えば簡単に自作できる。

標準ライブラリの再帰関数を使ったら Stack overflow 出た

標準ライブラリの再帰関数はほとんどが末尾再帰呼び出しになっていないので大きいデータを食わせると stack が溢れる。 比較的小さいデータの場合、非末尾呼び出しのほうがスピードが早いため、そうなっている。

Jane Street Core や OCaml batteries included などの末尾再帰版を使うか自分で末尾再帰版を定義する。

副作用を起こす式を関数引数に直接書かない。let で受ける

OCaml の引数評価順は未定義。そして実際には x86 では外から(右から)行われるため、直感とずれる場合があり、ハマる:

(* x86 *)
# let f x y =  x + y;;
val f : int -> int -> int = <fun>
# f (prerr_endline "hello"; 1) (prerr_endline "world"; 2);;
world
hello
- : int = 3

副作用を起こす式を二つ以上関数に食わせる場合は副作用順をはっきりさせるために let で結果を受けたほうが良い:

let one = prerr_endline "hello"; 1 in
let two = prerr_endline "world"; 2 in
f one two

二つ以上の引数に副作用を起こす式を指摘する lint が必要だと思われる。(難しいが)

愚直に let で結果を受けていればあまりこの問題には遭遇しないが、それでも私はこの手のバグに年に一度くらいハマる。 ポイントフリー教徒はより頻繁にハマるであろう。

raise Exit, raise Not_found でコードが読みやすくなるなら使う

例外による再帰関数からの大域脱出は OCaml ではランタイムのペナルティはほとんどないので、 try with を書いてそれでもコードが読みやすければ使って構わない。 ただしできるだけ例外は発生させるコードの周辺でローカルに処理すること。 さもなくば例外の取りこぼしによるバグに悩まされることになる。

OCaml 例外の静的型検査の研究はあるが採用されていない。

とはいえ例外に頼りすぎるな

例外に頼った再帰からの脱出を使わずとも同等の再帰関数を書くことは(初心者には難しいかもしれないが)全く可能である。 例外に頼りすぎるのはよくない。:

let exists p list =
  try
    List.iter (fun x -> if p x then raise Exit) list; false
  with Exit -> true

この手続き感のあるコードと

let rec exists p = function
[] -> false
x::_ when p x -> true
_::xs -> exists p xs

この関数型的コードは同じである。どちらが好まれるかは、もちろん後者である。前者のように書いていても構わないが… OCaml を書き続けるならどこかで努力をして後者に切り替えるべきである。

カスタム printf の作り方

Printf 系関数におけるフォーマット文字列の型付けは特殊で、そのため普通にフォーマット文字列を受け取り内部でそれを使って printf を呼び出すような関数は書きづらい:

(* 引数列が何個か fmt で決まるので書けないよー *)
let failwithf fmt 引数列 = failwith (Printf.sprintf fmt 引数列)

このような場合、継続スタイルの k*printf 関数をつかうとよい。 k は Keizoku の K。嘘です。 Kontinuation の K:

let failwithf fmt = Printf.ksprintf failwith fmt

特殊なリテラルを作りたい

CamlP4 で quotation 使う。 <:hoge<自由に書ける>>。 CamlP4 でのレクサ拡張は他の文法拡張と重ねることができないので、うまくいかない。 Quotation は <:x<>> と最低でも 6 文字必要なので悲しい。

もし可能ならば、パターンマッチは短いケースから書け

その方が読みやすい。 None -> ... と Some x -> ... x ... ならば 大抵 None の方が短いので None から書く。 大抵である。あなたが今まさに書こうとしている関数はもちろん例外だ。

もちろん書けない場合もある。安易な入れ替えは死を招く。

OCAMLRUNPARAM='b' でスタックトレースを表示する

OCAMLRUNPARAM 環境変数に b をセットすると uncaught exception でプログラムが終了する時に スタックトレースが表示される。assert false で死んだ場所は判ったが、どうやってそこにプログラム実行 が到達したかわからない場合に便利、というか常に b を入れとけ。わかったか。

Physical comparison (==) (!=) で泣く位なら始めから締めだす

自分が (=), (<>) と (==), (!=) の違いが判らないとか、同僚が判らない場合は もうさっさと (==) と (!=) は潰したほうがいい。Jane Street Core のように:

(* base.ml *)
let phys_equal = (==)
let (==) _ _ = `Consider_using_phys_equal
let (!=) _ _ = `Consider_using_phys_equal

open Base すると (==) と (!=) の型は 'a -> 'b -> [> `Consider_using_phys_equal] になる。 そこで:

if "hello" == "hello" then "equal" else "different";;

などと書くと型エラーに Consider_using_phys_equal と出てくるので、あれ?なんだこれは?とわかる。 本当に physical comparison を使いたい時は phys_equal "hello" "hello" と書く。 これを structural comparison (=) と間違って使った場合はもちろん救えない。

open Base を全 *.ml ファイルで強制しなければならないが、 これは grep などで調べれば機械的にチェックできる。

ログは flush を忘れるな

stderr といえども flush されない関数があるので注意。特に、

  • *_string は改行が入っていても flush しない! (*_newline, *_endline は flush する)
  • Printf.*printf は %! で明示しない限り改行でも flush しない!

Printf デバッグする時はこれに注意してないと「あれー表示されないのに実行されてるー?おかちいな」ということになる