はじめに
HaskellにUnion typeが欲しいです。残念ながら、僕の思うUnion typeがHaskellにはありません。
(TypeScriptのUnion TypesやDottyのUnion Typesみたいなシステムです。)
GHC 8.2.1から利用できるようになったUnboxedSums
という拡張が、見た目がUnion typeと似た感じになるので、使ってみます。
環境
Stack lts-10.5
(GHCのバージョンは 8.2.2になります)
簡単な使い方
以下を実行すると
{-# 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'
以下のような出力になります。
Nothing
Just 'a'
パターンマッチにのやり方
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
のようにトップレベルで宣言できます。
ただし、以下のようなことはできません。
-- (注意:このコードはコンパイルエラーします)
globalUnboxedSum :: (# Int | Bool #)
globalUnboxedSum = (# 1 | #)
以下のようなエラーがでて、トップレベルで宣言できなことがわかります。
Top-level bindings for unlifted types aren't allowed:
globalUnboxedSum = (# 1 | #)
|
13 | globalUnboxedSum = (# 1 | #)
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Show
のインスタンス "ではない"
つまり、print
などを使って手軽に表示できないのです。
(これに関する、暫定的な解決策は後述します)
main :: IO ()
main = do
let localUnboxedSum :: (# Int | Bool | Char #)
localUnboxedSum = (# 1 | | #)
-- (注意:このコードはここでコンパイルエラーします)
print localUnboxedSum
以下のようなエラーが出ます。
• 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
を使うと、色々な場面でこのエラーにやりたいことを阻まれます。)
-- (注意:このコードはコンパイルエラーします)
instance (Show a, Show b, Show c) => Show ((# a | b | c #)) where
show (# a | | #) = show a
show (# | b | #) = show b
show (# | | c #) = show c
エラーメッセージは以下の通りです
• 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)
で表示できます。
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などを見ながら、うまくコンパイルできて、使えるものを作りました。
-- (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
の使い方です。
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