HaskellのUnion TypeみたいなUnboxedSumsの使い方とハマりどころ



# はじめに

HaskellにUnion typeが欲しいです。残念ながら、僕の思うUnion typeがHaskellにはありません。
（TypeScriptの[Union Types](https://www.typescriptlang.org/docs/handbook/advanced-types.html)やDottyの[Union Types](http://dotty.epfl.ch/docs/reference/union-types.html)みたいなシステムです。）


GHC 8.2.1から利用できるようになった[`UnboxedSums`](https://ghc.haskell.org/trac/ghc/wiki/UnpackedSumTypes)という拡張が、見た目がUnion typeと似た感じになるので、使ってみます。

## 環境

Stack [lts-10.5](https://www.stackage.org/lts-10.5)
（GHCのバージョンは 8.2.2になります）

## 簡単な使い方

以下を実行すると

```hs
{-# LANGUAGE UnboxedSums #-}


func :: (# Int | Char | Bool #) -> Maybe Char
func (#  | ch | #) = Just ch
func _             = Nothing

main :: IO ()
main = do
    print (func (# 1 | | #))
    -- => Nothing
    print (func (#   | 'a' | #))
    -- => Just 'a'
```

以下のような出力になります。

```txt
Nothing
Just 'a'
```

### パターンマッチにのやり方

```hs
myfunc (# i |    |   #) =  ...
myfunc (#   | ch |   #) = ...
myfunc (#   |    | b #) = ...
```

### ハマりどころ

注意点としては、`myfunc (# i ||   #)`のように`|`の間のスペースをなくてしまうと、「error: Parse error in pattern: i ||」などとエラーがでることです。
同様に、リテラルを作るときも`(# 1 || #)`のように`|`の間のスペースをなくてしまうと、 「A section must be enclosed in parentheses thus: (1 ||)」とエラーが出ます。

## `UnboxedSums`の使いにくいところ

個人的に`UnboxedSums`が使いにくいと思ったところを列挙します。

### トップレベルで宣言できない

関数であれば、上記の`func`のようにトップレベルで宣言できます。
ただし、以下のようなことはできません。

```hs
-- (注意：このコードはコンパイルエラーします)
globalUnboxedSum :: (# Int | Bool #)
globalUnboxedSum = (# 1 |  #)

```

以下のようなエラーがでて、トップレベルで宣言できなことがわかります。

```txt
    Top-level bindings for unlifted types aren't allowed:
      globalUnboxedSum = (# 1 | #)
   |
13 | globalUnboxedSum = (# 1 |  #)
   | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

```

### `Show`のインスタンス "ではない"

つまり、`print`などを使って手軽に表示できないのです。
（これに関する、暫定的な解決策は後述します）

```hs
main :: IO ()
main = do
    let localUnboxedSum :: (# Int | Bool | Char #)
        localUnboxedSum =  (# 1 | | #)

    -- (注意：このコードはここでコンパイルエラーします)
    print localUnboxedSum
```

以下のようなエラーが出ます。

```txt
    • Couldn't match a lifted type with an unlifted type
      When matching the kind of ‘(# Int | Bool | Char #)’
    • In the first argument of ‘print’, namely ‘localUnboxedSum’
      In a stmt of a 'do' block: print localUnboxedSum
      In the expression:
        do let localUnboxedSum :: (# Int | Bool | Char #)
               localUnboxedSum = ...
           print (func (# 1 | | #))
           print (func (# | 'a' | #))
           print localUnboxedSum
   |
26 |     print localUnboxedSum
   |       ```
```

### `Show`のインスタンス "にできない"

「じゃあ、`Show`のインスタンスとして、実装してあげたらいいじゃないか」と思ったのですか、これもまた、「Couldn't match a lifted type with an unlifted type」に阻まれてできません。
(`UnboxedSums`を使うと、色々な場面でこのエラーにやりたいことを阻まれます。)

```hs
-- (注意：このコードはコンパイルエラーします)
instance (Show a, Show b, Show c) => Show ((# a | b | c #)) where
    show (# a |   |  #)  = show a
    show (#   | b |   #) = show b
    show (#   |   | c #) = show c
```

エラーメッセージは以下の通りです

```txt
    • Expecting a lifted type, but ‘(# a | b | c #)’ is unlifted
    • In the first argument of ‘Show’, namely ‘((# a | b | c #))’
      In the instance declaration for ‘Show ((# a | b | c #))’
   |
29 | instance (Show a, Show b, Show c) => Show ((# a | b | c #)) where
   |                                            ^^^^^^^^^^^^^^^
```


## 単純な解決策

以下のような、`myShow3`を作れば、`putStrLn (myShow3 localUnboxedSum)`で表示できます。

```hs
myShow3 :: (Show a, Show b, Show c) => (# a | b | c #) -> String
myShow3 (# a |   |  #)  = show a
myShow3 (#   | b |   #) = show b
myShow3 (#   |   | c #) = show c
```

ただし、`myShow2`, `myShow4`...のように和になっている型の数だけの関数の名前が必要になるので、あまり良くありません。



## より良い解決策

RadditのコメントやGitHubなどを見ながら、うまくコンパイルできて、使えるものを作りました。


```hs
-- (from: https://www.reddit.com/r/haskell/comments/6q6yau/unboxed_sums_ghc_panic/)
{-# LANGUAGE UnboxedSums #-}
{-# LANGUAGE TypeInType #-}

-- (from: https://github.com/gingerhot/haskell-base/blob/dbd1c3f0cf64e8c76c945530a805f7637dcdf777/testsuite/tests/deriving/should_fail/T12512.hs)
import GHC.Prim

class SumShow (a :: TYPE rep) where
    sumShow :: a -> String
  
instance (Show a, Show b) => SumShow ((# a | b #)) where
    sumShow (# a |    #) = show a
    sumShow (#   | b  #) = show b

instance (Show a, Show b, Show c) => SumShow ((# a | b | c #)) where
    sumShow (# a |   |  #)  = show a
    sumShow (#   | b |   #) = show b
    sumShow (#   |   | c #) = show c

instance (Show a, Show b, Show c, Show d) => SumShow ((# a | b | c | d #)) where
    sumShow (# a |   |   |   #) = show a
    sumShow (#   | b |   |   #) = show b
    sumShow (#   |   | c |   #) = show c
    sumShow (#   |   |   | d #) = show d

```

以下は`sumShow`の使い方です。

```hs
main :: IO ()
main = do
    putStrLn (sumShow ((# 1 |       #) :: (# Int | Bool #)))
    -- => 1
    putStrLn (sumShow ((#   | True  #) :: (# Int | Bool #)))
    -- => True

    putStrLn (sumShow ((# 'a' | |         #) :: (# Char | Int | String #)))
    -- => 'a'
    putStrLn (sumShow ((#     | | "hello" #) :: (# Char | Int | String #)))
    -- => "hello"
```


この方法なら、「単純な解決策」と違って、型の数が増えても、常に`sumShow`という名前でアクセスできるので、そこが改善されました。
あとは、TemplateHaskellなどを使って、量産できれば、もっと便利になると思います。

## GitHubの練習リポジトリ

これらの検証に使った、`UnboxedSums`用の練習リポジトリです。
[nwtgck/unboxed-sums-prac-haskell](https://github.com/nwtgck/unboxed-sums-prac-haskell)

# 参考
* [UnpackedSumTypes – GHC](https://ghc.haskell.org/trac/ghc/wiki/UnpackedSumTypes)
* [Unboxed Sums GHC Panic : haskell](https://www.reddit.com/r/haskell/comments/6q6yau/unboxed_sums_ghc_panic/)