あなたの知らないF#についての7つの事柄
この記事はF# Advent Calendar 2015の13日目の記事です。
タイトルは釣りで、今回はMicrosoft/visualfsharpリポジトリをウォッチしている中で見つけた個人的に面白かったIssueを取り上げます。
名前空間内でのprivateの意味
現状のF#では、名前空間内でprivate
を使った場合に、private
っぽくない挙動を示します。
同じプロジェクト内に、M1.fs, M2.fs, M3.fsを下記の内容のように作ってください。
(* M1.fs *) namespace NS module private M1 = let x = 10
(* M2.fs *) namespace NS module M2 = let x = M1.x (* OK *)
(* M3.fs *) namespace NS.Child module M3 = let x = NS.M1.x (* OK *)
このように、名前空間内にprivate
なモジュール(もしくは型)を作っても、同一アセンブリの同一名前空間以下にある型からはアクセスできるのです。
今後のバージョンでは、同一ファイルではない場合に警告になるように変更されるようです。
この挙動に依存している場合、private
ではなくinternal
を使うように変更しておくといいでしょう。
未使用変数の誤検知
F#3.0からは、コンパイルオプションに--warnon:1182
を付けることで未使用変数を警告扱いにできるようになっています。
extern指定したメソッドの引数に対する誤検知
extern
指定したメソッドの引数を未使用変数として誤検知します。
type ControlEventHandler = delegate of int -> bool [<DllImport("kernel32.dll")>] extern void SetConsoleCtrlHandler(ControlEventHandler callback, bool add) (* --warnon:1182してコンパイルすると、callbackとaddが未使用変数として検知されてしまう *)
これはバグなので、今後のリリースで修正されると思われます。
「(警告をエラー扱いにしている等の理由で)今回避したいんだ!」というときは、識別子を_
で始めればOKです。
type ControlEventHandler = delegate of int -> bool [<DllImport("kernel32.dll")>] extern void SetConsoleCtrlHandler(ControlEventHandler _callback, bool _add) (* --warnon:1182してコンパイルしても、_から始まる識別子は除外されるので警告にならない *) (* ただし、FSharpLintを使っている場合"_を識別子に含めるな!"という警告を出すようになる *)
元ネタ: External function arguments flagged as unused
クエリ式での誤検知
クエリ式でも誤検知します。
(* for xの位置でxが使われていないという警告が出る *) let res = query { for x in [1;2;3] do where (x > 2) select 1 } (* 同上 *) let res = query { for x in [1;2;3] do let y = x select y } (* これは出ない *) let res = query { for x in [1;2;3] do where (x > 2) select x }
元ネタ: Query expressions and --warnon:1182
プライマリコンストラクタの引数の型注釈
プライマリコンストラクタの引数に型注釈を付けないと、意図しない推論結果を引き起こします。
type ISomeInterface = interface end type C1() = interface ISomeInterface type C2() = interface ISomeInterface type SomeClass<'a when 'a :> ISomeInterface> (value) = member x.Value : 'a = value (* 警告「型変数'b'は型''a'に制約されました」 *) member x.Method(newValue: 'b when 'b :> ISomeInterface) = SomeClass<'b>(newValue)
let x = SomeClass<C1>(C1()) (* OK *) let y = x.Method(C1()) (* OK *) let z = x.Method(C2()) (* コンパイルエラー *)
Method
に明示的に型引数を付けてみましょう。
(* コンパイルエラー「この束縛に関する1つまたは複数の明示的クラスまたは関数型の変数は、他の型に制限されているため、ジェネリック化できませんでした。」 *) member x.Method<'b when 'b :> ISomeInterface>(newValue: 'b) = SomeClass<'b>(newValue)
駄目みたいです。
プライマリコンストラクタの引数には常に型注釈を付けるようにしましょう。
type SomeClass<'a when 'a :> ISomeInterface> (value: 'a) = member x.Value = value member x.Method(newValue: 'b when 'b :> ISomeInterface) = SomeClass<'b>(newValue)
元ネタ: Bug in the type checker with type variables
ジェネリックメソッドの制限
現状のF#では、ジェネリックメソッドは第一級の値として使えません。
module M = let f<'a>() = () type C = static member M<'a>() = ()
let x = M.f<int> (* OK *) let y = C.M<int> (* コンパイルエラー「予期しない型引数です」 *) () |> M.f<int> (* OK *) () |> C.M<int> (* コンパイルエラー *)
元ネタ: Provided methods with static params can't be used as first-class function values
リスト式(や配列式)の型推論のコーナーケース
下記のコードのコンパイル結果は一貫性がないように見えます。
let f (xss: 'a seq seq) = () f [ [ 'c' ] ] (* OK *) let charListList = [ [ 'c' ] ] f charListList (* コンパイルエラー「型'char list list'は型'seq<seq<'a>>'と互換性がありません」 *)
F#では、リスト(や配列)の要素の型がリスト式(や配列式)の型チェックよりも前に分かっている場合、各要素をその要素型にアップキャストを試みます。 これは、継承を考慮すると仕方なかった選択のようです。
例えば、型Widget
を継承したLabel
とButton
があったとします。
let xs: Widget list = [ Label(); Button() ] (* OK *)
これは、リスト式[ Label(); Button() ]
の型チェック前にこのリスト式の型がWidget list
になるとわかっている(xs
に型注釈が付いている)ため、リスト式の各要素はWidget
型にアップキャストされます。
そのため、このコードはコンパイルが通ります。
ではこちらはどうでしょう?
let xs = [ Label(); Button() ] (* コンパイルエラー「この式に必要な型はLabelですが、ここでは次の型が指定されています Button」 *)
これは、リスト式の型チェック前に型がわかっていないので、コンパイルエラーです。
さて、これで元のコードがなぜ一貫性がないように見えたかがわかります。 元のコードを再掲します。
let f (xss: 'a seq seq) = () f [ [ 'c' ] ] (* OK *) let charListList = [ [ 'c' ] ] f charListList (* コンパイルエラー「型'char list list'は型'seq<seq<'a>>'と互換性がありません」 *)
一回目のf
の呼び出しでは、引数の型が「'a seq seq
」になることがわかっています。
一番外側のseq
の要素の型は、'a seq
になることがわかっている、ということです。
実際に渡している要素は[ 'c' ]
であり、これはchar list
型です。
char list
型はchar seq
型にアップキャスト可能(list
はseq
を実装している)ため、コンパイルが通るわけです。
二回目のf
の呼び出しでは、リスト式ではなくいったん変数に入れたものを渡そうとしています。
charListList
の型はその名前通り、char list list
です。
リスト式ではないので要素型のアップキャストは行われず、'a seq
という型を期待しているところにchar list
がくるため、コンパイルエラーになるのです。
これを回避するためには、フレキシブル型を使います。
let f (xss: seq<#seq<'a>>) = () (* 'a #seq seq とは書けないっぽい *) f [ [ 'c' ] ] (* OK *) let charListList = [ [ 'c' ] ] f charListList (* OK *)
フレキシブル型の表記(#seq<'a>
)は型制約のあるジェネリック型('b when 'b :> seq<'a>
)の略記法です。
これによって、seq<'a>
の代わりにその派生型も許されるようになったため、char list list
型の変数も渡せるようになりました。
元ネタ: Possible inconsistency in type unification
メソッド(もしくはコンストラクタ)呼び出しの特別ルール
obj
(もしくは'a
)を1つ取るメソッドがあり、ほかにオーバーロードの候補がない場合、コンパイルできなさそうでできるコードが書けます。
type C = static member M(x: obj) = ()
C.M() (* OK *)
え、引数は?と思うかもしれませんが、引数は()
が渡っています。
この特別扱いルールは、
- 引数が1つのみ
- オーバーロードされてない
場合のみに有効です。 そのため、オーバーロードを追加するとこのコードは通らなくなります。
type C = static member M(x: obj) = () static member M(x: int, y: int) = ()
C.M() (* コンパイルエラー *) C.M(()) (* OK *)
このルール、何か嬉しい場合があるんでしょうか・・・
まとめ
今まで知らなかった言語仕様とかに気付かされることが多いので、登録されたIssueを読んでみると面白いです。 Resolution By Designタグが付けられたIssueがまずはおススメです。