すごいH本(すごいHaskellたのしく学ぼう!)で弱く型付けされた言語の例でYesNo型クラスを作って遊んでいるのを面白そうだと思ったので少し試してみます。
準備
- 空っぽのHaskellのソースファイル
空っぽのHaskellのソースファイルWeakTyping.hs
を用意してください。
module WeakTyping.hs where
-- ここにソースを書いていきますー
- GHCのREPL環境
各自の環境に合わせてWeakTyping.hs
をインポートしてghci
でREPLを起動できるようにしてください。
うまくいくと次のようなコンソールが表示されると思います。
WeakTyping>
WeakTyping.hs
を何度も書き換えます。書き換えたら適宜:reload
などで更新してください。
真らしい値と偽らしい値
真らしい値(truthy)と偽らしい(falsy)を判定してみましょう。例えば、JavaScriptだと真偽値でないものをifで評価できます。
> if (1) { "yes" } else { "no" }
'yes'
Boolable型クラス
真らしさを判定できるBoolable
(bool + ableの造語です)型クラスを作ってみましょう。
truthy
は引数のデータが真らしいならTrue
を返します。falsy
はtruthy
の否定ですね。
class Boolable a where
truthy :: a -> Bool
falsy :: a -> Bool
if' :: a -> b -> b -> b
truthy = not . falsy -- デフォルト実装
falsy = not . truthy -- デフォルト実装
if' cond t f
| truthy cond = t
| falsy cond = f
デフォルト実装をtruthy
とfalsy
とif'
の3つに定義しているのは型クラスの完全最小主義(minimal complate definition)に従っているからです。最小限の定義でインスタンスにするための考え方です。インスタンスはtruthy
かfalsy
のどちらかのメソッドを定義するだけで済みます。
Integer
のインスタンス
では、Integer
をBoolable
のインスタンスにしてみます。
instance Boolable Integer where
truthy 0 = False
truthy _ = True
ghci
でWeakTyping.hs
を試してみます。
> truthy 1
True
> truthy 0
False
> if' 1 "yes" "no"
"yes"
Maybe a
のインスタンス
Maybe a
はJust
が真っぽく、Nothing
が偽っぽいのでそのようにインスタンスを作ってみましょう。
instance Boolable (Maybe a) where
truthy (Just _) = True
truthy Nothing = False
> truthy (Just 1)
True
> truthy Nothing
False
[a]
のインスタンス
空リストは偽っぽいのでそのようにインスタンスを作ってみます。
instance Boolable [a] where
truthy [] = False
truthy _ = True
Bool
のインスタンス
Bool
はもちろんそのままですね。そのままなので恒等関数'id'を使って実装します。
instance Boolable Bool where
truthy = id
曖昧な比較
JavaScriptでは==
は曖昧な比較で型が違う場合でも適当にキャストされて真偽値を返してくれます。似たような比較を実装してみたいと思います。
Similar
型クラス
単に値が似ているという意味でSimilar
型クラスを作ってみます。
class Similar a b where
(~=) :: a -> b -> Bool
実はこのままだとコンパイルが通りません。
error:
• Too many parameters for class ‘Similar’
(Enable MultiParamTypeClasses to allow multi-parameter classes)
• In the class declaration for ‘Similar’
パラメータが多すぎると言われていますね。さらにMultiParamTypeClasses
を有効にしろと書いてあります。MultiParamTypeClasses
ってなんでしょうか?ググるとGHCの言語拡張ですね。
型引数が2つ以上の型クラスを作るには拡張が必要のようなので、ソースコードの冒頭に拡張を追加しておきましょう。
{-# LANGUAGE MultiParamTypeClasses #-}
これで無事にコンパイルが通ります。
Integer
とString
のインスタンス
次にInteger
とString
を比較できるようにSimilar
のインスタンスにしてみましょう。今回は数値側を文字列に変換して比較する実装とします。
instance Similar Integer String where
a ~= b = (show a) == b
また、コンパイルエラーとなりました。
error:
• Illegal instance declaration for ‘Similar Integer String’
(All instance types must be of the form (T t1 ... tn)
where T is not a synonym.
Use TypeSynonymInstances if you want to disable this.)
• In the instance declaration for ‘Similar Integer String’
型シノニムをインスタンスにできないようですね。String
は[Char]
の型シノニムなのでそれがいけないようです。先ほどと同じように言語拡張を使って解決できるようなので、TypeSynonymInstances
を冒頭に追加してみます。
{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances #-}
これでコンパイルが通ると思いきやさらに新しいエラーとなります。
error:
• Illegal instance declaration for ‘Similar Integer 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 FlexibleInstances if you want to disable this.)
• In the instance declaration for ‘Similar Integer String’
String
は[Char]
のシノニムですが、拡張なしでこのようなネストされた具体的な型を含む指定はできないようです。[a]
はOKで[Char]
や[Integer]
はNGです。(@udongohanより指摘をいただきました)
これを無効にするにはFlexibleInstances
を使えば良いと書いてあるので素直にしたがってみます。
冒頭を次のように書き換えて再度コンパイルしてみます。
{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-}
これで通りましたね!
ちなみにFlexibleInstances
はTypeSynonymInstances
を含んでいるので、TypeSynonymInstances
の拡張は外しても構いません。
ghci
で試してみます。
> 1 ~= "2"
False
> 1 ~= "1"
True
逆に"2" ~= 1
のように逆に比較する場合は次のインスタンスを定義します。(冗長な気がするので簡単に書く方法があれば知りたい。。。)
instance Similar String Integer where
a ~= b = a == show b
同じ型a
同士のインスタンス
さらに既存の==
で比較できるデータも~=
で比較できるようにしてみます。これは次のようにEq
型クラスに属する同じ型a
の==
をそのまま使えばいいですね。
instance Eq a => Similar a a where
(~=) = (==)
> 1 ~= 2
False
> 1 ~= 1
True
まとめ
Haskellも型クラスをうまく使うと柔軟で面白いことができます。もちろん実際にはこんな使い方をすることは稀だと思いますが。こんな風に遊んでみるのも勉強になりますね。