はじめに
簡単な具体例を「準備→実験→解説」の順で学びます。
準備
型クラスを作る
2つの型を取る型クラスを用意します。
class MyClass a b
MyClassのインスタンスを作る
- 「Int型とString型を取るMyClass」
- 「String型とBoolean型を取るMyClass」
- 「Boolean型と任意の型を取るMyClass」
の3インスタンスを用意します。
instance myClassIntString :: MyClass Int String
instance myClassStringBoolean :: MyClass String Boolean
instance myClassBooleanA :: MyClass Boolean a
型制約のある関数を作る
- test1: 「MyClassの第1型引数にInt型を取るインスタンスの第2型引数の型」の値を取ってEffect Unit型の値を返す関数
- test2: 「MyClassの第2型引数にBoolean型を取るインスタンスの第1引数の型」の値を取ってEffect Unit型の値を返す関数
- test3: 「MyClassの第1型引数にBoolean型を取るインスタンスの第2引数の型」の値を取ってEffect Unit型の値を返す関数
の3関数を用意します。
test1 :: forall a. MyClass Int a => a -> Effect Unit
test1 _ = pure unit
test2 :: forall a. MyClass a Boolean => a -> Effect Unit
test2 _ = pure unit
test3 :: forall a. MyClass Boolean a => a -> Effect Unit
test3 _ = pure unit
ここで、それぞれの関数の第1引数の型は、関数の定義とMyClass型クラスのインスタンスの実装を照らし合わせ、当てはまるものが存在するかどうかを検査されます。
例えば、以下のように書くと、test0関数は第1引数に任意の型の値を受け入れるようになってしまうので、
test0 :: forall a. a -> Effect Unit
test0 _ = pure unit
このようにしても問題なく型検査を通ってしまいます。
test0 { afjeoa: [ "bbbbb", "fjeao" ], abc: { asaaa: { ldkfah: 3333, ioaejo: false } } }
-- こんな型、定義してない!
{ abc :: { asaaa :: { ioaejo :: Boolean
, ldkfah :: Int
}
}
, afjeoa :: Array String
}
実験
型と関数の定義を再掲しておきます。
class MyClass a b
instance myClassIntString :: MyClass Int String
instance myClassStringBoolean :: MyClass String Boolean
instance myClassBooleanA :: MyClass Boolean a
test1 :: forall a. MyClass Int a => a -> Effect Unit
test1 _ = pure unit
test2 :: forall a. MyClass a Boolean => a -> Effect Unit
test2 _ = pure unit
test3 :: forall a. MyClass Boolean a => a -> Effect Unit
test3 _ = pure unit
実験1
test1関数を使ってみます。
test1 "hello" -- Success
test1 3 -- Error!
test1 false -- Error!
String型以外の値を渡した場合はコンパイルエラーになりました。
解説(実験1)
test1関数の定義では、MyClass型クラスの第1型引数の型がInt型であるインスタンスによって制約がかかっています。インスタンス宣言を見ると、インスタンスmyClassIntStringがこれに当てはまり、test1関数の定義にある型変数aはStringであることがわかります。したがって、この関数にString型以外の値を与えた場合はコンパイルエラーになります。
実験2
test2関数を使ってみます。
test2 "hello" -- Success
test2 3 -- Error!
test2 false -- Error!
これも(同じ結果になったのはたまたまですが)、String型以外の値を渡した場合はコンパイルエラーになりました。
解説(実験2)
test2関数の定義では、MyClass型クラスの第2型引数の型がBoolean型であるインスタンスによって制約がかかっています。インスタンス宣言を見ると、インスタンスmyStringBooleanがこれに当てはまり、test2関数の定義にある型変数aはStringであることがわかります。したがって、この関数にString型以外の値を与えた場合はコンパイルエラーになります。
実験3
test3関数を使ってみます。
test3 "hello" -- Success
test3 3 -- Success
test3 false -- Success
全て型検査を通りました。
解説(実験3)
test3関数の定義では、MyClass型クラスの第1型引数の型がInt型であるインスタンスによって制約がかかるはずでしたが、インスタンス宣言を見ると、対応するインスタンスはmyClassBooleanAであることがわかります。そして、test3関数の定義にある型変数aは型の指定がなく、任意の型を受け入れてしまうことがわかります。
本記事で使用したソースコード
module Main where
import Prelude
import Effect (Effect)
class MyClass a b
instance myClassIntString :: MyClass Int String
instance myClassStringBoolean :: MyClass String Boolean
instance myClassBooleanA :: MyClass Boolean a
main :: Effect Unit
main = do
test1 "hello" -- Success
--test1 3 -- Error!
--test1 false -- Error!
test2 "hello" -- Success
--test2 3 -- Error!
--test2 false -- Error!
test3 "hello" -- Success
test3 3 -- Success
test3 false -- Success
test1 :: forall a. MyClass Int a => a -> Effect Unit
test1 _ = pure unit
test2 :: forall a. MyClass a Boolean => a -> Effect Unit
test2 _ = pure unit
test3 :: forall a. MyClass Boolean a => a -> Effect Unit
test3 _ = pure unit
まとめ
代数的データ型や高階関数、高階多相型などを用いればさらに複雑な定義も可能です。
まだまだ勉強することが多いですが、制約を使いこなして、便利でわかりやすく型安全なコードを書きましょう!