LoginSignup
5
1

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-02-15

はじめに

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

参考

5
1
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
1