LoginSignup
13
2

More than 5 years have passed since last update.

How to make type classes without implicit parameter in Elm

Last updated at Posted at 2017-12-08

諸注意

 これは冗談みたいなものなので、実案件で使用しないでください。


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)

:diamond_shape_with_a_dot_inside: OK! :diamond_shape_with_a_dot_inside:

ステップ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でうまいこと解決していると思う)

終わりに

 この記事は、実際ネタ枠です。
現実的には、構造的部分型とかもっと単相的にやるとかの方がいいと思います。

13
2
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
2