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

F#に型クラスを入れる実装の話

この記事はF# Advent Calendar 2016の11日目のものです。ちょっと遅れてしまいました。。。

ICFP 2016(と併催されたML workshop?)で気になる内容があったので、ちょっとまとめてみました。

Classes for the Masses - ICFP 2016

ざっくり、F#に型クラスを導入してみたぜ、って内容です。

型クラスとは

JavaC#での interface みたいなものですが、interface は侵入的なのに対して、型クラスは非侵入的という違いがあります。

侵入的というのは、型の定義にその interface を実装しますよ、ということを書く必要があることを意味します。

// C#
interface Eq<A>
{
    bool Equal(A a, A b);
}

// intefaceは型に侵入する
class SomeClass : Eq<SomeClass>
{
    public bool Equal(A a, A b) { ... }
}

それに対して型クラスは非侵入的であり、型の定義にその型クラスを実装することは書きません。

-- haskell
class Eq a where
  (==) :: a -> a -> Bool

-- 型クラスは型の定義に書かなくていい
data SomeType = ...

-- SomeTypeをEq型クラスのインスタンスにする(型定義と分かれている)
instance Eq SomeType where
  x == y = ...

これの何が嬉しいかというと、ひとつは、型の定義をその型に対して可能な操作と分離できることです*1

これによって、例えば標準ライブラリの型に対しても、後付けで型クラスのインスタンスにできるようになります。 抽象を後付けできるとでも表現すればいいでしょうか。

この型クラスをF#に導入してみた、というのが今回紹介する内容です。

F#への型クラスの実装方法

実際に動作するコードは下記のリポジトリで公開されています。

github.com

先に示した Eq 型クラスはこの実装を使うと、

// Eq型クラスの実装(interfaceとしてコンパイルされる)
[<Trait>]
type Eq<'a> =
  abstract equal: 'a -> 'a -> bool

// SomeTypeをEq型クラスのインスタンスにする(structとしてコンパイルされる)
// Haskellと違い、インスタンスの定義に名前(ここではEqSomeType)が必要
[<Witness>]
type EqSomeType =
  interface Eq<SomeType> with
    member equal x y = ...

と書きます。

この Eq 型クラスを使うには、

let (==) a b = Eq.equal a b

のように、型クラス名.メンバー 引数リスト ... のように書くようです。 型クラスは structコンパイルされるため、デフォルト値を介して型クラスのメンバーにアクセスできます。 この関数は、下記のようにコンパイルされます。

// C#モドキ
public static bool operator ==<A, EqA>(A a, A b) where EqA : struct, Main.Eq<A>
    => default(EqA).equal(a, b);

struct を使うことで、追加の引数を不要にしています。

また、Eq 型クラスを要素に持つリストを Eq 型クラスのインスタンスにする(それ以外のリストは Eq 型クラスにしない)こともできます。

[<Witness>]
type EqList<'a, 'EqA when 'EqA :> Eq<'a>> =
  interface Eq<'a list> with
    member equal a b =
      match a, b with
      | x::xs, y::ys -> Eq.equal a b && Eq.equal xs ys
      | [], [] -> true
      | _, _ -> false

互換性及び他の.NET言語との連携

ここまででみたように、この実装ではあくまで.NETの型でそのまま表現できる形になっています。 ランタイムに手を加える必要がないため、互換性を崩すことなく採用できるように実装されている、ということです。

また型クラスを使った関数は、型クラスに対応しない既存の.NET言語からは型パラメータを明示的に渡せば使えます(使いやすいとは言っていない)。

この方法の問題点

この方法はランタイムに手を加えないため、(例えば Monad のような)高階型クラスがサポートできません。 ううむ、残念・・・

あ、それと、このリポジトリをcloneしてbuild.cmdを管理者権限で実行するとF#の環境がぶっ壊れた(VSでビルドできなくなった)ので、やるなら仮想環境で試してみることをお勧めします。

*1:オブジェクト指向プログラミングのよくある説明の一つに「データと操作をひとまとまりにできる」というものがありますが、それとはある意味正反対の特徴ですね。まぁ、この「データと操作をひとまとまりにできる」という説明には言いたいことがあるんですが、それは別の機会にでも