諸注意
これは冗談みたいなものなので、実案件で使用しないでください。
How to make and use type classes without implicit parameter in Elm
本記事は
Elm Advent Calendar 2017の12日目の記事です!
Elmへの型クラスの導入
この記事では、Philip Wadlerの論文「How to make ad-hoc polymorphism less ad hoc」
にて示される型クラスの導入を元にした、Elmに型クラスの導入を行うためのアプローチを示します。
元文では
基本的に表現は元の論文に準拠しつつ、
かつ修正が見込める場合は現代的な表現を用います。
本題: 単純な型クラスの導入
ステップ1: Num型クラスの定義
例としてNum
型クラスの定義を行います。
type NumD a = NumDict
{ add : a -> a -> a -- (+)の代わり
, mul : a -> a -> a -- (*)の代わり
, neg : a -> a -- negateの代わり
}
add : NumD a -> (a -> a -> a)
add (NumDict {add}) = add
mul : NumD a -> (a -> a -> a)
mul (NumDict {mul}) = mul
neg : NumD a -> (a -> a)
neg (NumDict {neg}) = neg
また、ここの各レコードはBasics
ライブラリとの命名衝突を避けた命名がなされています。
以下はHaskellのNum型クラスです。
class Num a where
add :: a -> a -> a -- 本来は(+)
mul :: a -> a -> a -- 本来は(*)
neg :: a -> a -- 本来はnegate
-- add :: Num a => a -> a -> a
-- mul :: Num a => a -> a -> a
-- neg :: Num a => a -> a -> a
ここでNumD
型 = Num
型クラスとみなします。
本記事では、型クラスとなる型名にはD
サフィックスを、
そのインスタンス定義(後述)に用いられる値構築子にはDict
サフィックスを付加します。
ステップ2: Numインスタンスの定義
{-| IntのNumインスタンスの定義 -}
numDInt : NumD Int
numDInt = NumDict
{ add = (+)
, mul = (*)
, neg = negate
}
{-| FloatのNumインスタンスの定義 -}
numDFloat : NumD Float
numDFloat = NumDict
{ add = (+)
, mul = (*)
, neg = negate
}
これでインスタンスの定義は完了です。
Int
及びFloat
のインスタンスの、add
, mul
, neg
関数を使用してみます。
> add numDInt 1 2
3 : Int
> mul numDFloat 3.0 3.0
9.0 : Float
> neg numDFloat 3.0
3.0 : Float
> add numDInt 1 2.0
(Int and Floatのタイプミスマッチエラー!)
Good!
…これ、実際はREPLで実行していなくて、コンパイルしてから似たような動作確認してます…。 elm-replはなぜ、ファイル読み込みを実装してくれないの?
ステップ2.5: Num型クラス制約のある関数を実装する
square :: Num a -> a -> a
square x = mul x x
-- x * x
このHaskellコードと同じ内容を、本アプローチのElmで実装します。
square : NumD a -> a -> a
square numDa x = mul numDa x x
ステップ3: 深いインスタンスを定義する
ここでの「深い」とは、以下のようにインスタンスに型制約を持ち、
かつ抽象型(具体型でない型)のインスタンスであることを示します。
numDPair : (NumD a, NumD b) -> NumD (a, b)
numDPair numDab =
let
addPair : (NumD a, NumD b) -> (a, b) -> (a, b) -> (a, b)
addPair (numDa, numDb) (x1, y1) (x2, y2) = (add numDa x1 x2, add numDb y1 y2)
mulPair : (NumD a, NumD b) -> (a, b) -> (a, b) -> (a, b)
mulPair (numDa, numDb) (x1, y1) (x2, y2) = (mul numDa x1 x2, mul numDb y1 y2)
negPair : (NumD a, NumD b) -> (a, b) -> (a, b)
negPair (numDa, numDb) (x, y) = (neg numDa x, neg numDb y)
in NumDict
{ add = addPair numDab
, mul = mulPair numDab
, neg = negPair numDab
}
これは以下のHaskellコードと同等です。
instance (Num a, Num b) => Num (a, b) where
add = addPair
where
addPair :: (Num a, Num b) => (a, b) -> (a, b) -> (a, b)
addPair (x1, y1) (x2, y2) = (add x1 x2, add y1 y2)
mul = mulPair
where
mulPair :: (Num a, Num b) => (a, b) -> (a, b) -> (a, b)
mulPair (x1, y1) (x2, y2) = (mul x1 x2, mul y1 y2)
neg = negPair
where
negPair :: (Num a, Num b) => (a, b) -> (a, b)
negPair (x, y) = (neg x, neg y)
…
> add (numDPair (numDInt, numDFloat)) (1, 2.0) (3, 4.0)
(4,6.699999999999999)
OK!
ステップX: まとめ
まとめとして、型安全な値の比較をする関数eq
を持つ型クラスEq
を作成します。
import List
{-|
class Eq a where
eq :: a -> a -> Bool
-}
type EqD a = EqDict
{ eq : a -> a -> Bool
}
{-|
eq :: Eq a => a -> a -> Bool
-}
eq : EqD a -> a -> a -> Bool
eq (EqDict {eq}) = eq
{-|
instance Eq Int where
eq = (==)
-}
eqDInt : EqD Int
eqDInt = EqDict
{ eq = (==)
}
{-|
instance Eq a => Eq (List a) where
eq xs ys = zipWith eq xs ys & List.all id
-}
eqDList : EqD a -> EqD (List a)
eqDList eqDa =
let
eqList : EqD a -> List a -> List a -> Bool
eqList eqDa xs ys = List.map2 (eq eqDa) xs ys |> List.all identity
in EqDict
{ eq = eqList eqDa
}
member : EqD a -> List a -> a -> Bool
member eqDa xs y = case xs of
[] -> False
(x::xs) -> eq eqDa x y || member eqDa xs y
> eq eqDInt 1 1
True
> eq (eqDList eqDInt) [1, 1] [1, 2]
False
> eq (eqDList (eqDList eqDInt)) [[10], [1, 2]] [[10], [1, 2]]
True
> member eqDInt [1, 2, 3] 2
True
複数の型制約を受け取る
memsq :: (Eq a, Num a) => [a] -> a -> Bool
このような2つ以上の型制約(aに対する(Eq a, Num a)
)を持つ場合は、単純に
memsq : (EqD a, NumD a) -> List a -> a -> Bool
memsq (eqDa, numDa) xs y = member eqDa xs (square numDa y)
のようにしてあげるとよいです。
> memsq (eqDInt, numDInt) [1, 3, 5, 7, 9] 3
True
(本章の以下は余談です)
もしかしたら、元論文を読んだ人が本章を読んだときに、違和感を感じたかもしれません。
というのも、ここでは元論文のこれについての内容を省略していることによるものです。
元論文では、例えば以下のような関数
memsq :: (Eq a, Num a) => [a] -> a -> Bool
の制約(Eq a, Num a)
を今までのルールに載せるには
「Eq a
もしくはNum a
のクラス宣言で、どちらかをどちらかのサブクラスにするといい」
と。
つまりEq
型クラスを
class Num a => Eq a where ...
memsq :: Eq a => [a] -> a -> Bool
と改変するか
(おそらくこの「改変」はプリプロセス時あたりを仮定している)
もしくは
class Eq a => Num a where ...
memsq :: Num a => [a] -> a -> Bool
とするとよい。
と書いてある思うのですが、
本アプローチではそれも必要ないので、ばっさり省略しています。
終章: 複数の引数を持つ型クラス(multi param type class)
複数の引数(実装の対象)を持つ型クラス(multi param type class)
を実装できることを示し、これで締めとします。
(「示す」だけに「締め」ということで)
例として、a
からb
への変換ができることを表す型クラスCoerce
を実装します。
type CoerceD a b = CoerceDict
{ coerce : a -> b
}
coerce : CoerceD a b -> a -> b
coerce (CoerceDict {coerce}) = coerce
coerceDIntFloat : CoerceD Int Float
coerceDIntFloat = CoerceDict
{ coerce = toFloat
}
補足
本記事のタイトルに付けた 'without implicit parameter'
の意ですが、
本アプローチではご覧の通り、型クラスインスタンスを表す値を
関数呼び出し時に省略することができません。
e.g. ここのeqDInt
-- vvvvvv
> eq eqDInt 1 1
True
(例えばScalaはこれをimplicit parameter
でうまいこと解決していると思う)
終わりに
この記事は、実際ネタ枠です。
現実的には、構造的部分型とかもっと単相的にやるとかの方がいいと思います。