reasonml

型安全の夢を一行で打ち砕く黒魔術入門

ドキュメントを眺めていて、こんな記述を見つけました。

型システムは完全に「安全」です。 これは、コードがコンパイルされている限り、すべての型に誤りがないことが保証されることを意味します。 従来のベストエフォートな型システムでは、例えば「Integer は決して null にならない」と言っていたとしても、言っているだけで、本当に決して null にならないということではありません。 それとは対照に、純粋な Reason プログラムは null のバグはありません。

私は型理論に明るくないので「型の完全性」が何を示すのかはわかりませんが、型安全の抜け道は知ってます。私だけでなく、 OCaml ユーザーならわかるでしょう。

それは Obj.magic という関数です。この関数の型はこう定義されています。

let magic: 'a => 'b;

これを見て笑った人は、この記事を読む必要はありません。もうわかったはずです。

なんだこりゃ?と思った方は、一つ冷静になって考えてみてください。この関数は、 任意の型 'a を、任意の型 'b に変換 します。当然ながら、 'a と 'b は同じ型である必要はありません。 'a が int で 'b が bool でもいいわけです。

この型の関数の実装を考えてみてください。処理内容はどうでもいいものとします。関数の型が 'a => 'b になれば OK です。

例えば、 int を bool に変換する関数は簡単に書けるでしょう。処理内容はどうでもいいので、単に true を返すだけでも構いません。次に、 int を任意の型 'b に変換する関数はどうでしょうか。もちろん 'b は関数が呼び出される文脈によって変わります。 'b は bool かもしれませんし、 string かもしれません。

let f = (value: int) : 'b => {
  /* どうする? */
};

何か思いつきます? 無理じゃないですか? 任意の型を戻り値にするとはどうすればいいのか。任意の型を生成するとは? int 値を任意の値に変更する? どうやって?

Obj.magic はどうにかしちゃいます。型の上では任意の型 'a を任意の型 'b に変換します。ただし、 Obj.magic は何もしません。引数の値をそのまま戻り値にします。型だけが変わります。つまり、 int の値を string として扱えるようになります。

/* 見た目は文字列、中身は整数! */
let f = (a: int) : string => Obj.magic(a);

/* 文字列の長さを表示する */
Js.log(String.length(f(123)));

オンラインでこのコードを入力してみてください。出力には undefined と表示されるはずです。

出力される JavaScript のコードを見ればはっきりわかります。

// Generated by BUCKLESCRIPT VERSION 1.9.2, PLEASE EDIT WITH CARE
'use strict';


function f(a) {
  return a;
}

console.log((123).length);

exports.f = f;
/*  Not a pure module */

整数 123length プロパティを持っていないので結果は undefined です。 Reason の型検査をすり抜けてしまいました。ちなみに OCaml の出力だと Obj.magic が残ります。元々 OCaml のライブラリの API なので。

let f (a : int) = (Obj.magic a : string)
let _ = Js.log (String.length (f 123))

Obj.magic はどうしても使わなければならない場面があり、実際に ReasonReact でも使われています。普通は使いませんし推奨もしませんが、 Obj.magic を使っているライブラリは型検査をすり抜けたバグを含む可能性がある、ということを頭の隅に置いておくと、いざというときにデバッグにかかる時間を削減できるかもしれません。