Haskell

Haskellにおける型クラス制約の役割

More than 1 year has passed since last update.

最近Haskellの勉強を始めました。そこで最近「なるほど!」と思った型クラス制約について説明をしたいと思います。なお記事は対話形式で進められます。

リストの合計値を求める関数を実装する

私はPMのアランにリストの合計値を求める関数の実装を頼まれます。(既存の関数があるけど、そこは気にしない、むしろHaskellの強みは既存の関数を容易に実装できることにあるのです。)

アラン: ヘイ、ヒロト! リストの合計値を求める関数を実装してくれないか
ヒロト: オーケイ!そんなの朝飯前ですよ!

sum.hs
sum' :: [Int] -> Int
sum' = foldr1 (\x acc -> x + acc)

でもこれには問題があります。そう、これはIntのリストしか受け付けません。
アランに呼び出されます。

アラン: ヒロト!君が実装した関数はIntしか受け付けないよ!FloatDoubleのリストにも適用できるようにしてくれないか
ヒロト: おっけい!型変数を使えばいいんだね!

sum.hs
sum' :: [a] -> a
sum' = foldr1 (\x acc -> x + acc)

しかしコンパイルしようとするとエラーが出ます。

error
    • 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は型変数なので数値以外の型(BoolChar)などを受け取れるのです。Charなどを受け取っても合計値を求めることはできません。

そこで型クラス制約が出てきます。これによって型変数aは型クラスNumのインスタンスでなくはならないことを定めることができます。結果、aNumのインスタンスであることが確約されるのでNumで実装されている関数(ここでは加算)が使えるようになります。

sum.hs
sum' :: (Num a) => [a] -> a
sum' = foldr1 (\x acc -> x + acc)

これで実装が完了しました。

sum
*Main> sum' [1,2,3,4,5]
15

つまり関数の宣言において型変数を利用する際には、その関数でどのような処理を行うのか、それにはどの型クラスのインスタンスである必要があるのかを考慮する必要があります。必要な処理が分かれば型クラス制約は導出されます。

最初、型クラス制約を学んだ際にはそこがよくわからなくて、「型クラス制約をすれば、その型クラスの関数が利用できるようになるんだ(微妙に違う)」と思っていました。

参考になった記事

Typeclasses and overloading, what is the connection?