F# 型互換性の謎 Scalaと比較で

めざせ自然言語プログラミング by F# - なんたらノート 第二期のコメントより。

let hoge x = string x;;
と定義した obj -> string となる型の関数が、引数として純粋なobj型以外の変数を受け入れるようになっていたことが高評価ポイントでした。

って、objとobjのサブクラスとの互換性が取られたところまでは良かったものの、しばらく調べてみたら…

F#の1.9.6.2では、obj list 型の引数に int list とかの値を入れられませんでした。用語を忘れたけど、なんか一般的な問題としてこういうのがあったはず。

let hoge xs = xs |> List.map (fun x -> x |> string)
hoge [1;2;3];;

こう書いてしまうと、推論の結果 xs は obj list となります。で、xs に int list を食わせたいのに obj list じゃないとか言われてエラーでした。 しかも、

hoge ([1;2;3] :> obj list);;

も、

Type constraint mismatch. The type
int list
is not compatible with type
obj list.
The type 'obj' does not match the type 'int'.

というエラーになります。(呼び出し側でどうにかするには、obj list型の変数に入れなおさないといけない)

というわけでやっぱり、

let hoge xs = xs |> List.map (fun x -> (x :> obj) |> string)
hoge [1;2;3];;

というように、xsの型をあいまいに(objにキャスト可能な総称型として推論されるように誘導)して、xs が 'a list 型となるようにすれば、任意のリストが渡せるようになりました。

これは、かなり厄介です。

  • 型間の互換性のために、objではなく総称型でないといけない場合があること。
  • 「呼び出される側の実装に起因して」obj型縛りになるということ。つまり、宣言時点で型の自動的な詳細化を防ぐことができないこと。
  • 型の勝手な詳細化の要因が、引数に関連する変数を型限定する関数に渡してしまうという、ごくありふれた(しかもそのコードだけで型詳細化されてることがわからない)操作であること。
  • それを防ぐには型推論を先読みするようにコーディングしないといけないこと。

たぶんそのうちこっそり修正されるんだろうけど、まだ言語仕様が不安定(サブマイナーバージョン変わっただけで、機能仕様に変更が入ってるなんて)だし、またVBとは別の社会的な問題が起こりそうだなぁ。考えすぎ? とりあえずここはしばらく静観しとこう。

ちなみにScalaだと、

def hoge (xs:List[Any]) = { xs map { _ toString } }
val nums : List[Int] = 1::2::3::Nil
hoge(nums)

はOKで、List[Any]型はList[Int]型と互換性があり(Int is an Any)、明示的なキャストすら必要ありません。事前定義する関数の型宣言が少々面倒でも、今のうちは、動作が予測しやすいScalaで遊んでおこうっと。