0. Typeclass と Class
Typeclass とは Haskell におけるインターフェースの定義であり、一連の振る舞いを宣言するために使用されます。
OOP における Class はオブジェクトのテンプレートであり、現実の物事を記述し、その内部状態をカプセル化するために使われます。FP には内部状態という概念がないため、関数型の文脈における Class はインターフェースを指します。特定のクラスから派生させる(deriving (SomeTypeclass))ことは、そのクラスで定義された振る舞いを持つことを意味し、OOP におけるインターフェースの実装に相当します。したがって、インターフェースで定義された振る舞いを持つことになります。
1. 宣言
class キーワードは、新しい typeclass を定義するために使われます:
class Eq a where
(==) :: a -> a -> Bool
(/=) :: a -> a -> Bool
x == y = not (x /= y)
x /= y = not (x == y)
ここで、a は型変数であり、instance を定義する際に具体的な型が指定されます。最初の2つの型宣言はインターフェースによって定義された振る舞い(関数型を定義することで記述)です。後ろの2つの関数実装はオプションであり、間接的な再帰定義を通じてこれら2つの関数の関係を記述しています。これにより、片方の関数の実装を提供するだけで十分になります(この方式は minimal complete definition、最小完全定義と呼ばれます)。
P.S. GHCi 環境では、:info <typeclass> コマンドを使用して、そのクラスで定義されている関数や、どの型がそのクラスに属しているかを確認できます。
2. 実装
instance キーワードは、特定の typeclass のインスタンスを定義するために使われます:
instance Eq TrafficLight where
Red == Red = True
Green == Green = True
Yellow == Yellow = True
_ == _ = False
ここでは、class Eq a における型変数 a を具体的な TrafficLight 型に置き換え、== 関数を実装しています(Eq クラス内で両者の関係が宣言されているため、== と /= を同時に実装する必要はありません)。
自作の型を Show クラスのメンバーにしてみましょう:
data Answer = Yes | No | NoExcuse
instance Show Answer where
show Yes = "Yes, sir."
show No = "No, sir."
show NoExcuse = "No excuse, sir."
試してみましょう:
> Yes
Yes, sir.
P.S. GHCi 環境では、:info <type> コマンドを使用して、その型がどの typeclass に属しているかを確認できます。
サブクラス
同様に、サブクラスという概念もあります。これは、クラス B のメンバーになるためには、まずクラス A のメンバーでなければならないという制約を指します:
class (Eq a) => Num a where
-- ...
Num クラスのメンバーは、まず Eq クラスのメンバーである必要があります。構文的には型制約が追加されただけです。同様に、別の例を見てみましょう:
instance (Eq m) => Eq (Maybe m) where
Just x == Just y = x == y
Nothing == Nothing = True
_ == _ = False
ここでは、Maybe a における型変数 a が Eq クラスのメンバーである必要があり、その場合にのみ Maybe a も Eq クラスのメンバーになることができます。
3. Functor
ファンクタ(Functor、何だか凄そうな響きですが)も一つの typeclass であり、写像(マッピング)が可能なもの(map over できるもの)を表します。
class Functor f where
fmap :: (a -> b) -> f a -> f b
fmap は、a から b への写像関数と、f a 型の引数を受け取り、f b 型の値を返します。
少し紛らわしいですが、f a 型とは型パラメータを持つ型のことで、例えば Maybe や List などが挙げられます。例えば:
mapMaybe :: Eq t => (t -> a) -> Maybe t -> Maybe a
mapMaybe f m
| m == Nothing = Nothing
| otherwise = Just (f x)
where (Just x) = m
ここで、Maybe t -> Maybe a は f a -> f b の一例です。試してみましょう:
> mapMaybe (> 0) (Just 3)
Just True
ここでの a から b への写像は、Maybe Num から Maybe Bool への変換を指します:
Just 3 :: Num a => Maybe a
Just True :: Maybe Bool
したがって、Functor が定義する振る舞いは、大きな型(f a、ここでの a は型変数)はそのままに、写像(fmap 関数)を通じて小さな型を変更する(f a から f b へ、ここでの a と b は具体的な型)ことを許可するものです。
List の文脈に当てはめると、List の内容に対して写像を行い、別の List を得ることを許可します。新しい List の要素の型は変化しても構いません。しかし、いずれにせよ fmap の結果は List a(ここでの a は型変数)になります。
これは非常に自然に聞こえます。なぜなら、List はもともと Functor クラスに属しており、かつ以下のようになっているからです:
map :: (a -> b) -> [a] -> [b]
これはまさに fmap :: (a -> b) -> f a -> f b という型定義の具体的な実装の一つです。実際、この map こそが fmap そのものです:
instance Functor [] where
fmap = map
Maybe と List はどちらも Functor クラスに属していますが、それらの共通点は何でしょうか?
どちらもコンテナのようなものです。そして fmap が定義する振る舞いは、まさにコンテナの中身(値)に対して写像を行い、終わったら再びコンテナに詰めるというものです。
他にも特殊なシナリオがあります。例えば Either です:
data Either a b = Left a | Right b -- Defined in ‘Data.Either’
Either の型コンストラクタには2つの型パラメータがありますが、fmap :: (a -> b) -> f a -> f b の f は1つのパラメータしか受け取りません。そのため、Either の fmap は左側の型が固定されていることを要求します:
mapEither :: (t -> b) -> Either a t -> Either a b
mapEither f (Right b) = Right (f b)
mapEither f (Left a) = Left a
左側に対しては写像を行いません。なぜなら、写像によって型が変わる可能性がある一方で、Either a(すなわち fmap :: (a -> b) -> f a -> f b における f)は不変である必要があるからです。そのため、Nothing と同じように扱われます。例えば:
> mapEither show (Right 3)
Right "3"
> mapEither show (Left 3)
Left 3
もう一つの似た例は Map です:
-- Data.Mapに別名Mapを付与
data Map.Map k a -- ...
Map k v で写像を行う際、k は変わるべきではないため、値に対してのみ写像を行います:
mapMap :: Ord k => (t -> a) -> Map.Map k t -> Map.Map k a
mapMap f m = Map.fromList (map (\(k ,v) -> (k, f v)) xs)
where xs = Map.toList m
例えば:
> mapMap (+1) (Map.insert 'a' 2 Map.empty)
fromList [('a',3)]
> mapMap (+1) Map.empty
fromList []
P.S. これらの簡単な実装は、標準ライブラリの実装と比較することで正しさを検証できます。例えば:
> fmap (+1) (Map.insert 'a' 2 Map.empty )
fromList [('a',3)]
P.S. また、Functor を実装する際にはいくつかの規則に従う必要があります。例えば、List の要素の順序が変わらないようにすることや、二分探索木がその構造的性質を維持することなどです。
4. Kind
演算に参加するのは値(関数を含む)であり、型は値の属性であるため、値は型によって分類できます。値が持つこの属性を通じて、その値のいくつかの性質を推論できます。同様に、kind は型の型であり、型に対する分類と言えます。
GHCi 環境では、:kind コマンドを使用して、型の型を確認できます。例えば:
> :k Int
Int :: *
> :k Maybe
Maybe :: * -> *
> :k Maybe Int
Maybe Int :: *
> :k Either
Either :: * -> * -> *
> :k Either Bool
Either Bool :: * -> *
> :k Either Bool Int
Either Bool Int :: *
Int :: * は Int が具体的な型であることを示し、Maybe :: * -> * は Maybe が1つの具体的な型の引数を受け取り、具体的な型を返すことを示します。また、Either :: * -> * -> * は Either が2つの具体的な型の引数を受け取り、具体的な型を返すことを示しています。これは関数呼び出しに似ており、カリー化の特性も持っているため、部分適用(partially apply)が可能です。
さらに奇妙な kind もあります。例えば:
data Frank a b = Frank {frankField :: b a} deriving (Show)
値コンストラクタ Frank の引数 frankField の型を b a に限定しています。したがって、b は * -> * であり、a は具体的な型 * です。この場合、Frank 型コンストラクタの kind は以下のようになります:
Frank :: * -> (* -> *) -> *
ここで、最初の * は引数 a、真ん中の * -> * は引数 b、最後の * は具体的な型を返すことを意味します。以下のように値を埋めることができます:
> :t Frank {frankField = Just True}
Frank {frankField = Just True} :: Frank Bool Maybe
> :t Frank {frankField = "hoho"}
Frank {frankField = "hoho"} :: Frank Char []
Either の Functor 実装を振り返ってみましょう:
> :k Either
Either :: * -> * -> *
> :t fmap
fmap :: Functor f => (a -> b) -> f a -> f b
Either の kind は * -> * -> *(2つの具体的な型の引数が必要)ですが、fmap が求めている f は * -> *(1つの具体的な型の引数だけが必要)です。そのため、Either を部分適用し、引数を1つ埋めて * -> * にする必要があります。すると、mapEither の実装は以下のようになります:
mapEither :: (t -> b) -> Either a t -> Either a b
mapEither f (Right b) = Right (f b)
mapEither f (Left a) = Left a
Either a は標準的な * -> * になります。例えば:
> :k Either Int
Either Int :: * -> *
P.S. typeclass に対しても実行できます。例えば:
> :k Functor
Functor :: (* -> *) -> Constraint
> :k Eq
Eq :: * -> Constraint
ここで、Constraint も一種の kind であり、特定のクラスのインスタンスでなければならないこと(すなわち型制約。関数シグネチャの => の左側でよく見かけます)を表します。例えば Num などです。詳細は What does has kind 'Constraint' mean in Haskell を参照してください。
コメントはまだありません