読者です 読者をやめる 読者になる 読者になる

あなたの知らない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を使うように変更しておくといいでしょう。

元ネタ: Suggestion: restrict "private" for items in namespaces to mean "private to the namespace declaration group"

未使用変数の誤検知

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を継承したLabelButtonがあったとします。

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型にアップキャスト可能(listseqを実装している)ため、コンパイルが通るわけです。

二回目の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 *)

え、引数は?と思うかもしれませんが、引数は()が渡っています。 この特別扱いルールは、

場合のみに有効です。 そのため、オーバーロードを追加するとこのコードは通らなくなります。

type C =
  static member M(x: obj) = ()
  static member M(x: int, y: int) = ()
C.M()   (* コンパイルエラー *)
C.M(()) (* OK *)

このルール、何か嬉しい場合があるんでしょうか・・・

元ネタ: Implicit class constructor with a single obj argument could be called without argument which will pass null as argument

まとめ

今まで知らなかった言語仕様とかに気付かされることが多いので、登録されたIssueを読んでみると面白いです。 Resolution By Designタグが付けられたIssueがまずはおススメです。