Haskellの型クラスでJavaScriptっぽいことをやって遊んでみる

すごいH本([すごいHaskellたのしく学ぼう!](https://www.amazon.co.jp/%E3%81%99%E3%81%94%E3%81%84Haskell%E3%81%9F%E3%81%AE%E3%81%97%E3%81%8F%E5%AD%A6%E3%81%BC%E3%81%86-Miran-Lipova%C4%8Da/dp/4274068854))で弱く型付けされた言語の例でYesNo型クラスを作って遊んでいるのを面白そうだと思ったので少し試してみます。

# 準備

* 空っぽのHaskellのソースファイル

空っぽのHaskellのソースファイル`WeakTyping.hs`を用意してください。

```hs:WeakTyping.hs
module WeakTyping.hs where
-- ここにソースを書いていきますー
```

* GHCのREPL環境

各自の環境に合わせて`WeakTyping.hs`をインポートして`ghci`でREPLを起動できるようにしてください。
うまくいくと次のようなコンソールが表示されると思います。

```
WeakTyping>
```

`WeakTyping.hs`を何度も書き換えます。書き換えたら適宜`:reload`などで更新してください。

# 真らしい値と偽らしい値

真らしい値(truthy)と偽らしい(falsy)を判定してみましょう。例えば、JavaScriptだと真偽値でないものをifで評価できます。

```js
> if (1) { "yes" } else { "no" }
'yes'
```

## Boolable型クラス

真らしさを判定できる`Boolable`(bool + ableの造語です)型クラスを作ってみましょう。
`truthy`は引数のデータが真らしいなら`True`を返します。`falsy`は`truthy`の否定ですね。

```hs:WeakTyping.hs
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`のインスタンスにしてみます。

```hs:WeakTyping.hs
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`が偽っぽいのでそのようにインスタンスを作ってみましょう。

```hs:WeakTyping.hs
instance Boolable (Maybe a) where
  truthy (Just _) = True
  truthy Nothing = False
```

```
> truthy (Just 1)
True
> truthy Nothing
False
```

## `[a]`のインスタンス

空リストは偽っぽいのでそのようにインスタンスを作ってみます。

```hs:WeakTyping.hs
instance Boolable [a] where
  truthy [] = False
  truthy _ = True
```

## `Bool`のインスタンス

`Bool`はもちろんそのままですね。そのままなので恒等関数'id'を使って実装します。

```hs:WeakTyping.hs
instance Boolable Bool where
  truthy = id
```

# 曖昧な比較

JavaScriptでは`==`は曖昧な比較で型が違う場合でも適当にキャストされて真偽値を返してくれます。似たような比較を実装してみたいと思います。


## `Similar`型クラス

単に値が似ているという意味で`Similar`型クラスを作ってみます。

```hs:WeakTyping.hs
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つ以上の型クラスを作るには拡張が必要のようなので、ソースコードの冒頭に拡張を追加しておきましょう。

```hs:WeakTyping.hs
{-# LANGUAGE MultiParamTypeClasses #-}
```

これで無事にコンパイルが通ります。

## `Integer`と`String`のインスタンス

次に`Integer`と`String`を比較できるように`Similar`のインスタンスにしてみましょう。今回は数値側を文字列に変換して比較する実装とします。

```hs:WeakTyping.hs
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`を冒頭に追加してみます。

```hs:WeakTyping.hs
{-# 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’
```



`(T a1 ... an)`の形で型引数に渡す必要があるようです。具体的には`Maybe String`や`Either Integer String`のようなことでしょう。少なくとも今の`Integer String`はこれに当てはまらない形なのでエラーとなるようです。



`String`は`[Char]`のシノニムですが、拡張なしでこのようなネストされた具体的な型を含む指定はできないようです。`[a]`はOKで`[Char]`や`[Integer]`はNGです。(@udongohanより指摘をいただきました)

これを無効にするには`FlexibleInstances`を使えば良いと書いてあるので素直にしたがってみます。

冒頭を次のように書き換えて再度コンパイルしてみます。

```hs:WeakTyping.hs
{-# LANGUAGE MultiParamTypeClasses, TypeSynonymInstances, FlexibleInstances #-}
```

これで通りましたね！
ちなみに`FlexibleInstances`は`TypeSynonymInstances`を含んでいるので、`TypeSynonymInstances`の拡張は外しても構いません。

`ghci`で試してみます。

```
> 1 ~= "2"
False
> 1 ~= "1"
True
```

逆に`"2" ~= 1`のように逆に比較する場合は次のインスタンスを定義します。（冗長な気がするので簡単に書く方法があれば知りたい。。。）

```hs:WeakTyping.hs
instance Similar String Integer where
  a ~= b = a == show b
```

## 同じ型`a`同士のインスタンス

さらに既存の`==`で比較できるデータも`~=`で比較できるようにしてみます。これは次のように`Eq`型クラスに属する同じ型`a`の`==`をそのまま使えばいいですね。

```hs:WeakTyping.hs
instance Eq a => Similar a a where
  (~=) = (==)
```

```
> 1 ~= 2
False
> 1 ~= 1
True
```

# まとめ

Haskellも型クラスをうまく使うと柔軟で面白いことができます。もちろん実際にはこんな使い方をすることは稀だと思いますが。こんな風に遊んでみるのも勉強になりますね。