bitterharvest’s diary

A Bitter Harvestは小説の題名。作者は豪州のPeter Yeldham。苦闘の末に勝ちえた偏見からの解放は命との引換になったという悲しい物語

HaskellドリルⅢ 型クラスとは

1.型クラスと型

オブジェクト指向でのクラスとインスタンスの概念に馴染むのに時間がかかったように、Haskellでの型クラスと型の概念に馴染むためにも、時間を必要とする。

型クラスと型の関係は図で示すと以下のようになっている。
f:id:bitterharvest:20141211141532p:plain

上の図において、青い四角い部分が型クラスである。例えば、左上にあるEqという型クラスは等価を示す型クラスである。型クラスEqは、I/Oと(モナド)->を除くすべての型をインスタンスとしている。Eqで定義されている関数は==(等しい)と/=(等しくない)である。

型クラスは階層構造をなす。上記の図で、矢印の根元は上位の型クラス、矢印の先は下位の型クラスである。下位の型クラスは上位の型クラスで定義された関数を継承する。例えば、型クラスOrdは型クラスEqの下位の型クラスである。このため、OrdはEqで定義された関数==と/=を継承する。また、これらとは別に、Ordのところで関数<,>,<=,>=,compareを定義してる。従って、Ordでは、==,/=,<,>,<=,>=,compareの七つの関数を利用できる。

2.型クラスの定義

型クラスがどのように定義されているかを見てみる。等価を示す型クラスEqは次のように定義されている。(クラスの定義を見るには、:info クラス名を用いる。)

Prelude> :info Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool

上記のプログラムで、Eqが型クラスの名前であり、aは例によって型変数である。whereの後で、二つの関数==と/=に対しての型シグネチャがある。両方の型シグネチャとも、型変数がともにaである値を二つ入力として受け取り、型Boolの値(すなわち、TrueかFalse)を返すとなっている。従って、この型シグネチャより、二つの値の関係を調べ、その真偽を返すということが分かる。

:infoでは表示されないが、Eqはデフォルトで関数が定義されている。それは次のようになっている。

Prelude> :info Eq
class Eq a where
  (==) :: a -> a -> Bool
  (/=) :: a -> a -> Bool
  x == y = not (x /= y)
  x /= y = not (x == y)

何か変な関数なのだが、前者は、xとyが等しいのは、xとyが異なっていないとき、後者は、xとyが等しくないのは、xとyが同じでないときといっている。

通常、この型クラスのインスタンスとなる型では、==と/=を再定義して上書きする。しかし、それらの全てを列挙するのが大変な場合がある。その時、片方だけ、例えば==だけ、を定義して、他は、即ち/=は、定義せずデフォルトの関数を用いたほうが便利な時がある。このことを許すために、デフォルトの関数定義では、お互いに相手を用いて定義している。

3.インスタンスを実装する

それでは新しい型を導入して、Eqのインスタンスにすることを考える。
今、成績Markという新しい型を導入し、この型が値としてA,B,C,Dを有することにする。この時、次のように定義できる。

data Mark = A | B | C | D

Dに続けてderiving Eqをつければ、自動的に型Markは型クラスEqのインスタンスとなるが、ここでは、明示的にEqのインスタンスにする。ここでは==だけ定義し、/=はデフォルトの定義を用いることとする。

instance Eq Mark where
  A == A = True
  B == B = True
  C == C = True
  D == D = True
  _ == _ = False 

実行すると次のようになる。正しく、動いていることが分かる。

Prelude> :load "Mark.hs"
[1 of 1] Compiling Main             ( Mark.hs, interpreted )
Ok, modules loaded: Main.
*Main> A == A
True
*Main> A /= A
False
*Main> A == B
False
*Main> A /= B
True
*Main> 

4.サブクラス化

OrdはEqのサブクラスであるが、サブクラスの実装は次のようになる。

class (Eq a) => Ord a where
......

:infoで観察すると次のように定義されている。

Prelude> :info Ord
class Eq a => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a

これらから、Ordで定義されている関数には大小関係の他に最大値、最小値などがあることが分かる。

5.問題

それでは、問題を解くことにしよう。

問題1.3 + 4の演算はどの型クラスのインスタンスとなるか?

問題2.3.0 + 4の演算はどの型クラスのインスタンスとなるか?

問題3.3.0 + 3/4の演算はどの型クラスのインスタンスとなるか?

問題4.sqrt 4の演算はどの型クラスのインスタンスとなるか?

問題5.3.0 + (sqrt 4)の演算はどの型クラスのインスタンスとなるか?

問題6.曜日Dayを新しいデータ型として定義し、これをEqのインスタンスとして明示的に定義しなさい。

問題7.データ型DayをOrdのインスタンスとして定義しなさい。順番は日曜日から始まる曜日順とする。

問題8.2進数をデータ型Binaryとして定義し、これをEqのインスタンスとして明示的に定義しなさい。

問題9.データ型BinaryをOrdのインスタンスとして明示的に定義しなさい。

問題10.データ型BinaryをNumのインスタンスとして明示的に定義しなさい。


(注)Haskell 98 Reportを参照すると以下のようになっている。

class  Eq a  where
      (==), (/=)  ::  a -> a -> Bool
      x /= y  = not (x == y)
      x == y  = not (x /= y)

class  (Eq a) => Ord a  where
  compare              :: a -> a -> Ordering
  (<), (<=), (>=), (>) :: a -> a -> Bool
  max, min             :: a -> a -> a
  compare x y | x == y    = EQ
              | x <= y    = LT
              | otherwise = GT
  x <= y  = compare x y /= GT
  x <  y  = compare x y == LT
  x >= y  = compare x y /= LT
  x >  y  = compare x y == GT
  max x y | x <= y    =  y
          | otherwise =  x
  min x y | x <= y    =  x
          | otherwise =  y

class  Read a  where
    readsPrec :: Int -> ReadS a
    readList  :: ReadS [a]

class  Show a  where
    showsPrec :: Int -> a -> ShowS
    show      :: a -> String 
    showList  :: [a] -> ShowS
    showsPrec _ x s   = show x ++ s
    show x            = showsPrec 0 x ""

class  Enum a  where
    succ, pred     :: a -> a
    toEnum         :: Int -> a
    fromEnum       :: a -> Int
    enumFrom       :: a -> [a]            -- [n..]
    enumFromThen   :: a -> a -> [a]       -- [n,n'..]
    enumFromTo     :: a -> a -> [a]       -- [n..m]
    enumFromThenTo :: a -> a -> a -> [a]  -- [n,n'..m]

class  (Eq a, Show a) => Num a  where
    (+), (-), (*)  :: a -> a -> a
    negate         :: a -> a
    abs, signum    :: a -> a
    fromInteger    :: Integer -> a

class  (Num a, Ord a) => Real a  where
    toRational ::  a -> Rational

class  (Real a, Enum a) => Integral a  where
    quot, rem, div, mod :: a -> a -> a
    quotRem, divMod     :: a -> a -> (a,a)
    toInteger           :: a -> Integer

class  (Num a) => Fractional a  where
    (/)          :: a -> a -> a
    recip        :: a -> a
    fromRational :: Rational -> a

class  (Fractional a) => Floating a  where
    pi                  :: a
    exp, log, sqrt      :: a -> a
    (**), logBase       :: a -> a -> a
    sin, cos, tan       :: a -> a
    asin, acos, atan    :: a -> a
    sinh, cosh, tanh    :: a -> a
    asinh, acosh, atanh :: a -> a
class  (Real a, Fractional a) => RealFrac a  where
    properFraction   :: (Integral b) => a -> (b,a)
    truncate, round  :: (Integral b) => a -> b
    ceiling, floor   :: (Integral b) => a -> b

class  (RealFrac a, Floating a) => RealFloat a  where
    floatRadix          :: a -> Integer
    floatDigits         :: a -> Int
    floatRange          :: a -> (Int,Int)
    decodeFloat         :: a -> (Integer,Int)
    encodeFloat         :: Integer -> Int -> a
    exponent            :: a -> Int
    significand         :: a -> a
    scaleFloat          :: Int -> a -> a
    isNaN, isInfinite, isDenormalized, isNegativeZero, isIEEE 
                        :: a -> Bool
    atan2               :: a -> a -> a

gcd, lcm :: (Integral a) => a -> a-> a
(^)      :: (Num a, Integral b) => a -> b -> a
(^^)     :: (Fractional a, Integral b) => a -> b -> a

fromIntegral :: (Integral a, Num b) => a -> b
realToFrac   :: (Real a, Fractional b) => a -> b

しかし、Preludeでは、異なる場合も見受けられる。例えば、

Prelude> :info Num
class Num a where
  (+) :: a -> a -> a
  (*) :: a -> a -> a
  (-) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a

となっていて、EqがNumの上位の型クラスであるとは宣言していない。