LoginSignup
13
4

More than 5 years have passed since last update.

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

Posted at

はじめに

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)
                ]

型のパターンマッチング

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

言語拡張ScopedTypeVariablesDataKindsを有効にします。

-- (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 rrに書く感じです。)
(この関数の捉え方を変えれば、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
13
4
0

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
13
4