はじめに
HaskellでもTypeScriptやDottyやCrystalのようにUnion typeが欲しくなるときがありますよね
-- いちいち、こんな風に書いたりしたくないですし、
data IntOrChar = I Int | C Char
-- 型が増えたバージョンを用意する必要が出たときに、いちいち増やすために名前が被らないようにするのは、嫌ですよね
data IntOrCharOrString = I' Int | C' Char | S' String
open-unionというライブラリを食わず嫌いでやっていなかったので、今回触れてみました(前回の記事でUnboxedSumsは用途が違うとご指摘も受けましたし)。
環境
Stack: lts-10.5 (GHCは8.2.2になります)
open-union-0.3.0.0
GitHubの練習用リポジトリ
nwtgck/open-union-prac-haskell
リポジトリのapp/に動くコードが入ってます。
普通の型 => Union Type
まずは、DataKinds
言語拡張を有効して、Data.OpenUnion
をインポートします
{-# LANGUAGE DataKinds #-}
import Data.OpenUnion
普通の型をUnion typeにするときはliftUnion
を使います。以下は例です。
let u1 :: Union '[Char, Int]
u1 = liftUnion (35 :: Int)
print u1
-- => Union (35 :: Int)
let u2 :: Union '[Char, Int]
u2 = liftUnion 'j'
print u2
-- => Union ('j' :: Char)
これを使えば、以下のように、型安全なヘテロリストも作れます
let hList1 :: [ Union '[Maybe Bool, String, ()] ]
hList1 = [ liftUnion "apple"
, liftUnion (Just True)
, liftUnion ()
, liftUnion "orange"
, liftUnion (Nothing :: Maybe Bool)
]
型のパターンマッチング
こんな感じで、各型のときの処理を書いていきます
(パターンマッチングと言っていいか分からないですが...)
言語拡張ScopedTypeVariables
とDataKinds
を有効にします。
-- (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
を使います。
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
を使います。
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
にしたい例です。
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
ときに掛け算をする」といったこともできます。
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)
let u4 = liftUnion (4 :: Int) :: Union '[Int, Bool, Char]
let u5 = liftUnion (3 :: Int) :: Union '[(), Int]
print (myMul u4 u5)
-- => Just 12