OCaml 標準ライブラリ探訪 #1.2 Pervasives 補遺

関連リンク:
OCaml 標準ライブラリ探訪 #0
その他は上の記事のトラックバックから。

他の方同士の会話からでも勝手に抜き取ります。すいません。

[...] failwithf "hoge: %s" "hii" ();;でも普通に動きます。でもなんで()付きで [...]
chunjpさん

これは、ご紹介した failwithf の実装

val failwithf : ('a, unit, string, unit -> 'b) format4 -> 'a

let failwithf fmt = Printf.kprintf (fun s () -> failwith s) fmt

の実装で、例えば、

failwithf "something has failed: %s" reason ();;

の様に、なぜ、わざわざ最後に () が必要なのか?ということですね。

val failwithf' : ('a, unit, string, 'b) format4 -> 'a

let failwithf' fmt = Printf.kprintf failwith fmt in
failwithf' "something has failed: %s" reason;;

の方が簡単ではないかと。

() があると嬉しい理由、これは id:KeisukeNakano さんの記事と非常に関連があるのですが、わかりますでしょうか。

詳しく解説するにはどうしても format4 の型について説明をしなければならないので、それは Printf の回でやりたいと思います。ここでは型の話はできるだけ避けます。

failwithf' では、間違ってフォーマット文字列が要求するよりも多く引数を多く与えてしまっても、型システムではその間違いを発見できません:

# failwithf' "something has failed: %s" "crash" 100 'x';;
Exception: Failure "something has failed: crash"

これは failwith が string -> 'a という型を持っているのと同じく、failwithf' も例外を投げたら大域脱出してしまうので、結果の型は型変数 'a になります。型変数はどんな型にも instantiate できます。例えば上のように、 int -> char -> 'b の様に。failwith "message" 100 'x' とか、 raise (Failure "message") 100 'x' と同様、間違って過適用してしまった引数はそのままどこかにいってしまいます。(どこかにいくというか、捨てられるだけですが)

failwithf は () の abstraction があるので、これを完全ではありませんが防ぐことが出来ます:

# failwith "something has failed: %s" "crash" 100 'x';;
                                              ^^^
Error: This expression has type int but an expression was expected of type unit

もちろん、フォーマット文字列が () を引数に取らないということが前提です。普通はありませんが、 %a は () を取る可能性があるので、完全に間違いを防げる訳ではありません。また、() を適用した後は failwithf', raise (Failure "..."), failwith "..." と同じくブラックホール化しますから、同様に、

# failwith "something has failed: %s" "crash" () 100 'x';;

と書けてしまいます。さすがにそこまでは拾えない。