HaskellでUnion typeを実現するopen-unionを使ってみました

## はじめに

HaskellでもTypeScriptやDottyやCrystalのようにUnion typeが欲しくなるときがありますよね


```hs
-- いちいち、こんな風に書いたりしたくないですし、
data IntOrChar         = I  Int | C  Char
-- 型が増えたバージョンを用意する必要が出たときに、いちいち増やすために名前が被らないようにするのは、嫌ですよね
data IntOrCharOrString = I' Int | C' Char | S' String
```

[open-union](https://hackage.haskell.org/package/open-union)というライブラリを食わず嫌いでやっていなかったので、今回触れてみました（[前回の記事](https://qiita.com/nwtgck/items/19832cdb4345e705ca1c)でUnboxedSumsは用途が違うとご指摘も受けましたし）。


## 環境

Stack: [lts-10.5](https://www.stackage.org/lts-10.5) (GHCは8.2.2になります)
[open-union-0.3.0.0](https://hackage.haskell.org/package/open-union-0.3.0.0)

### GitHubの練習用リポジトリ

[nwtgck/open-union-prac-haskell](https://github.com/nwtgck/open-union-prac-haskell)

リポジトリの[app/](https://github.com/nwtgck/open-union-prac-haskell/tree/master/app)に動くコードが入ってます。

## 普通の型 => Union Type


まずは、`DataKinds`言語拡張を有効して、`Data.OpenUnion`をインポートします

```hs
{-# LANGUAGE DataKinds #-}
import Data.OpenUnion
```

普通の型をUnion typeにするときは`liftUnion`を使います。以下は例です。

```hs
let u1 :: Union '[Char, Int]
    u1 = liftUnion (35 :: Int)
print u1
-- => Union (35 :: Int)
```

```hs
let u2 :: Union '[Char, Int]
    u2 = liftUnion 'j'
print u2
-- => Union ('j' :: Char)
```

これを使えば、以下のように、型安全なヘテロリストも作れます

```hs
let hList1 :: [ Union '[Maybe Bool, String, ()] ]
    hList1 = [ liftUnion "apple"
                , liftUnion (Just True)
                , liftUnion ()
                , liftUnion "orange"
                , liftUnion (Nothing :: Maybe Bool)
                ]
```

## 型のパターンマッチング

こんな感じで、各型のときの処理を書いていきます
（パターンマッチングと言っていいか分からないですが...）

言語拡張`ScopedTypeVariables`と`DataKinds`を有効にします。

```hs
-- (from: https://github.com/bfops/open-union)

{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DataKinds #-}


import Data.OpenUnion

showMyUnion :: Union '[Char, Int, [()]] -> String
showMyUnion
    =  (\(c :: Char)   -> "char: " ++ show c)
    @> (\(i :: Int)    -> "int: " ++ show i)
    @> (\(l :: [()])   -> "list length: " ++ show (length l))
    @> (\(s :: String) -> "string: " ++ s)
    @> typesExhausted

main :: IO ()
main = do
    putStrLn $ showMyUnion $ liftUnion (4 :: Int)
    putStrLn $ showMyUnion $ liftUnion 'a'
    putStrLn $ showMyUnion $ liftUnion [(), ()]
```
(これは公式のコードを使わさせていただきました)

## Union type => より広いUnion type

`Union '[Char, Int]` => `Union '[Char, Int, Bool]`にするようなことをしたいときは、`reUnion`を使います。

```hs
let u1     :: Union '[Char, Int]
    u1     = liftUnion (35 :: Int)
let wideU1 :: Union '[Char, Int, Bool]
    wideU1 = reUnion u1
```

## 型の順番が違うUnion


`Union '[Char, Int]` => `Union '[Int, Char]`のようなときです。
これも`reUnion`を使います。

```hs
let u1     :: Union '[Char, Int]
    u1     = liftUnion (35 :: Int)

let u1'    :: Union '[Int, Char]
    u1'    = reUnion u1
```


## Union type => 普通の型

`Union [Char, Int, Bool]` => `Int`にするときのように普通の型にするときに使い方

以下は使用例です。

これは`Union '[Char, Int, Bool]` => `Int`にしたい例です。

```hs
let u3 :: Union '[Char, Int, Bool]
    u3      = liftUnion (3 :: Int)
let narrowU3 :: Either (Union '[Char, Bool]) Int
    narrowU3 = restrict u3
print narrowU3
-- => Right 3 
```
（変換で得たい型は`Either l r`の`r`に書く感じです。）
（この関数の捉え方を変えれば、`Int`を消して、`Union '[Char, Bool]`なものはLeftに来るように考えることもできそうです）

`restrict`があれば、以下の例のように、「要素が`Int`ときに掛け算をする」といったこともできます。

```hs
import Data.Either.Combinators (rightToMaybe)

myMul :: Union '[Int, Bool, Char] -> Union '[(), Int] -> Maybe Int
myMul x y = do
    i  <- rightToMaybe (restrict x)
    j  <- rightToMaybe (restrict y)
    return (i * j)
```

```hs
let u4 = liftUnion (4 :: Int) :: Union '[Int, Bool, Char]
let u5 = liftUnion (3 :: Int) :: Union '[(), Int]
print (myMul u4 u5)
-- => Just 12
```