演算子のオーバーロード #4

グローバルレベルで定義された演算子演算子オーバーロードを隠してしまう。それはわかった。それならそれで、グローバルレベルで定義された演算子は多相(ジェネリック)にできないのだろうか。

結論から言うとできる。ただし気を付けなければならないことがある。

まず、関数の場合は山かっこ(<および>)を関数名の直後に置ける。

let addCount<'a when 'a :> System.Collections.ICollection>
    (x: 'a) (y: 'a) = x.Count + y.Count

この書き方は演算子ではできない。代わりにこのように書く。

// 演算子の場合はこう書くしかない
let (+) (x: 'a) (y: 'a): int
    when 'a :> System.Collections.ICollection =
    x.Count + y.Count

// なお関数なら両方の書き方が許される
let addCount (x: 'a) (y: 'a): int
    when 'a :> System.Collections.ICollection =
    x.Count + y.Count

こうすればジェネリック演算子ができるのだが、F#のジェネリクスは.NETのジェネリクスを基盤にしているから、F#の型制約も基本的にはその範囲でしか書けない。つまり、(new制約や構造体制約は別とすれば)指定したクラスやインターフェースを継承している場合に、その型のインスタンスメソッドが呼び出せる、という程度のことしかできない。オーバーロードされた演算子はスタティックメンバー扱いだからジェネリックなメソッドからは呼び出せない。したがって、グローバルレベルで定義されたジェネリック演算子の実装から呼び出すこともできない。C#と一緒である*1

ただ、F#の強力なところは、インライン関数を使うことでその制限を超えることができることだ。インライン関数はコンパイル時に静的に型が解決されるため、明示的なメンバーの制約をかけることで、型階層によらず、また静的メンバーとインスタンスメンバーを問わず、あるメンバーを持っている型が使われることを要請できる。

VSで書くといまいちわかりづらいのでF# Intaractiveを使う。

> let inline (%*) x y = x * y;;

val inline ( %* ) :
  x: ^a -> y: ^b ->  ^c
    when ( ^a or  ^b) : (static member ( * ) :  ^a *  ^b ->  ^c)

%演算子の型が、通常のジェネリック型引数で使われる 'a'bではなく、静的に解決される型を表す ^a^bになっている。%演算子を使う際には、左項(x)または右項(y)のどちらかの型に対して*演算子オーバーロードが定義されており、もう一方の項の型もその定義に適合するのであれば、どんな型であってもこの新しい演算子を適用できる。

もちろん、この演算子コンパイル時にインライン展開(β変換)されるので、通常のジェネリックな型に適用することはできない。ジェネリックな型は実行時に解決されるものだからである。

(続く)

*1:F#では列挙型制約やデリゲート制約をつけられるが、C#ではできないという違いはある