最近Haskellの勉強を始めました。そこで最近「なるほど!」と思った型クラス制約について説明をしたいと思います。なお記事は対話形式で進められます。
リストの合計値を求める関数を実装する
私はPMのアランにリストの合計値を求める関数の実装を頼まれます。(既存の関数があるけど、そこは気にしない、むしろHaskellの強みは既存の関数を容易に実装できることにあるのです。)
アラン: ヘイ、ヒロト! リストの合計値を求める関数を実装してくれないか
ヒロト: オーケイ!そんなの朝飯前ですよ!
sum' :: [Int] -> Int
sum' = foldr1 (\x acc -> x + acc)
でもこれには問題があります。そう、これはInt
のリストしか受け付けません。
アランに呼び出されます。
アラン: ヒロト!君が実装した関数はInt
しか受け付けないよ!Float
やDouble
のリストにも適用できるようにしてくれないか
ヒロト: おっけい!型変数を使えばいいんだね!
sum' :: [a] -> a
sum' = foldr1 (\x acc -> x + acc)
しかしコンパイルしようとするとエラーが出ます。
• No instance for (Num a) arising from a use of ‘+’
Possible fix:
add (Num a) to the context of
the type signature for:
sum' :: forall a. [a] -> a
• In the expression: x + acc
In the first argument of ‘foldl1’, namely ‘(\ x acc -> x + acc)’
In the expression: foldl1 (\ x acc -> x + acc)
|
5 | sum' = foldl1 (\x acc -> x + acc)
|
これは要するに「foldl1
関数で要素同士を足そうとしてるけど、a
は型クラスNum
のインスタンスじゃないからできないよ!」と言われているのです(違ったら教えてください。。)
そう、a
は型変数なので数値以外の型(Bool
やChar
)などを受け取れるのです。Char
などを受け取っても合計値を求めることはできません。
そこで型クラス制約が出てきます。これによって型変数a
は型クラスNum
のインスタンスでなくはならないことを定めることができます。結果、a
はNum
のインスタンスであることが確約されるのでNum
で実装されている関数(ここでは加算)が使えるようになります。
sum' :: (Num a) => [a] -> a
sum' = foldr1 (\x acc -> x + acc)
これで実装が完了しました。
*Main> sum' [1,2,3,4,5]
15
つまり関数の宣言において型変数を利用する際には、その関数でどのような処理を行うのか、それにはどの型クラスのインスタンスである必要があるのかを考慮する必要があります。必要な処理が分かれば型クラス制約は導出されます。
最初、型クラス制約を学んだ際にはそこがよくわからなくて、「型クラス制約をすれば、その型クラスの関数が利用できるようになるんだ(微妙に違う)」と思っていました。