Haskellの実験メモです。
HaskellでC++やJavaなどでできる関数のオーバーロードのようなことをやってみました。型クラスについての簡単な説明も含みます。
新しい言語を使うときはまさに手探りで、試行錯誤の過程についても記載しています。パターンマッチによる関数の分割定義を見たとき、関数のオーバーロードに似ていると感じたことが発端となっています。
C++
C++では同じ名前で引数の型が異なる関数を複数定義することができます。(関数のオーバーロード)
#include <iostream>
#include <string>
using namespace std;
string foo(int i) {
if (i == 1) return "bar";
return "?";
}
string foo(const string &s) {
if (s == "1") return "baz";
return "?";
}
int main() {
cout << foo(1) << endl;
cout << foo("1") << endl;
}
bar
baz
Haskell
結論から言うと次のコードで同等な処理ができます。
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE IncoherentInstances #-}
class Foo a where
foo :: a -> String
instance (Num a, Eq a) => Foo a where
foo 1 = "bar"
foo _ = "?"
instance Foo String where
foo "1" = "baz"
foo _ = "?"
main = do
putStrLn $ foo 1
putStrLn $ foo "1"
bar
baz
ここにたどり着くまでにはかなり試行錯誤しました。
引数の型エラー
最初に試したものはエラーになりました。
foo 1 = "bar"
foo "1" = "baz"
foo _ = "?"
main = do
putStrLn $ foo 1
putStrLn $ foo "1"
src\Main.hs:1:5:
No instance for (Num [Char]) arising from the literal `1'
Possible fix: add an instance declaration for (Num [Char])
In the pattern: 1
In an equation for `foo': foo 1 = "bar"
型が異なるものはパターンマッチできません。
型クラス
Haskellには型クラスという機能があります。これは型のグループ化で、関数をそのグループ(型クラス)に対して定義します。クラスという名称からオブジェクト指向言語(Javaなど)のクラスを連想させますが、どちらかというとクラスというよりインターフェースに近いです。Haskellのinstance
は実装を任意の型に後付けできます。
- 参考 1: 型クラス - ウォークスルー Haskell
- 参考 2: Haskellの型クラスとF#のインターフェース
関数foo
を含む型クラスFoo
を定義してInt
とString
を登録しましたが、エラーとなりました。
class Foo a where
foo :: a -> String
instance Foo Int where
foo 1 = "bar"
foo _ = "?"
instance Foo String where
foo "1" = "baz"
foo _ = "?"
main = do
putStrLn $ foo 1
putStrLn $ foo "1"
src\Main.hs:8:10:
Illegal instance declaration for `Foo String'
(All instance types must be of the form (T t1 ... tn)
where T is not a synonym.
Use -XTypeSynonymInstances if you want to disable this.)
In the instance declaration for `Foo String'
String
はPreludeの中でtype String = [Char]
と定義されている型シノニムです。デフォルトでは型シノニムは型クラスのインスタンスに指定できませんが、エラーにもあるようにTypeSynonymInstances拡張で許可されます。
- 参考 3: Haskellの言語拡張たち - rfなブログ
しかしこれだけではエラーになります。
{-# LANGUAGE TypeSynonymInstances #-}
src\Main.hs:10:10:
Illegal instance declaration for `Foo String'
(All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Use -XFlexibleInstances if you want to disable this.)
In the instance declaration for `Foo String'
String
の実体は[Char]
ですが、これはリストの型クラスに対するインスタンスです。デフォルトでは型クラスのインスタンスに指定できませんが、エラーにもあるようにFlexibleInstances拡張で許可されます。
FlexibleInstancesにはTypeSynonymInstancesも含むため、簡単のため前者のみを指定します。
{-# LANGUAGE FlexibleInstances #-}
数値リテラル
FlexibleInstancesにより型クラスのエラーは解消しますが、今度は呼び出す側でエラーとなります。
src\Main.hs:14:16:
No instance for (Foo a0) arising from a use of `foo'
The type variable `a0' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
(略)
src\Main.hs:14:20:
No instance for (Num a0) arising from the literal `1'
The type variable `a0' is ambiguous
Possible fix: add a type signature that fixes these type variable(s)
(略)
エラーにあるように呼び出す側の引数に型を指定すれば動きます。
putStrLn $ foo (1 :: Int)
bar
baz
これで一応は目的を達成しましたが、やや不満があります。show
では:: Int
などと書かなくてもちゃんと動くので、型を指定しなくても済むようにす方法があるはずです。
main = do
putStrLn $ show 1
putStrLn $ show "1"
1
"1"
show
のような使い勝手を目指してもう少し続けます。
Num
エラーを見るとNum
についての記載があります。
No instance for (Num a0) arising from the literal `1'
型クラスのインスタンスをInt
からNum
に変更するとエラーになります。
instance Num a => Foo a where
src\Main.hs:7:10:
Constraint is no smaller than the instance head
in the constraint: Num a
(Use -XUndecidableInstances to permit this)
In the instance declaration for `Foo a'
デフォルトでは型クラスのインスタンスに型制約を指定できませんが、エラーにもあるようにUndecidableInstances拡張で許可されます。
{-# LANGUAGE UndecidableInstances #-}
しかしまた別のエラーです。
src\Main.hs:9:9:
Could not deduce (Eq a) arising from the literal `1'
from the context (Num a)
bound by the instance declaration at src\Main.hs:8:10-23
Possible fix: add (Eq a) to the context of the instance declaration
(略)
src\Main.hs:16:16:
Overlapping instances for Foo [Char] arising from a use of `foo'
(略)
1つ目のエラーはパターンマッチで値が比較できないことが原因です。エラーにもあるように型制約Eq
を追加します。
instance (Num a, Eq a) => Foo a where
2つ目のエラーはString
がNum a
のa
として解釈(Num String
)されようとすることに起因します。GHCiでNum
のインスタンスを確認します。
Prelude> :i Num
class Num a where
(略)
-- Defined in `GHC.Num'
instance Num Integer -- Defined in `GHC.Num'
instance Num Int -- Defined in `GHC.Num'
instance Num Float -- Defined in `GHC.Float'
instance Num Double -- Defined in `GHC.Float'
型クラスに登録されていないインスタンスを除外するためIncoherentInstances拡張を有効化します。
{-# LANGUAGE IncoherentInstances #-}
正常に動くようになりました。これで最初に掲載したコードが得られました。
さくっとIncoherentInstances拡張を持ち出して解決しましたが、たどり着くまではかなりの試行錯誤が必要でした。
IncoherentInstances拡張
手詰まりとなったため、Preludeでshow
がどのように定義されているかを調べました。
Show [a]
を定義して、型制約Show a
によりShow Char
から[Char]
を絞り込んでいるようです。
instance Show Char where
(略)
instance (Show a) => Show [a] where
showsPrec p = showList
とりあえずPreludeからShow
を書き写して試行錯誤しました。その過程で次のようなエラーが起きました。
{-# LANGUAGE UndecidableInstances,FlexibleInstances #-}
class Show' a where
show' :: a -> String
show' _ = "?"
instance Show' Integer where
show' _ = "Integer"
instance Show' Int where
show' _ = "Int"
instance Show' Float where
show' _ = "Float"
instance Show' Double where
show' _ = "Double"
instance Show' Char where
show' _ = "Char"
instance Show' a => Show' [a] where
show' _ = "List"
instance Num a => Show' a where
show' _ = "Num"
main = do
print (show' 1)
--print (show' "1")
src\Main.hs:29:12:
Overlapping instances for Show' a0 arising from a use of show'
Matching instances:
(略)
(The choice depends on the instantiation of `a0'
To pick the first instance above, use -XIncoherentInstances
when compiling the other instance declarations)
(略)
どうやら型制約Show' a
とNum a
が競合して、それを回避するためにIncoherentInstances拡張が提案されたようです。このような経緯で発見しました。