• 作成:
  • 更新:

union typeは楽園ではない

HaskellやOCamlにunion typeがあっても良いのでは?

似非原さんが最近OCamlを始めていて、「調べた範囲ではOCamlにはTypeScriptのようなunion typeはなかった」と言っていました。

確かにない。

その場では、 OCamlは静的型付けの言語で、ベーシックな型は型チェックが終わったら消去されるので、 typeofinstanceofで実行時に型情報を入手できるTypeScriptとかとは違い、直和型を使うとか、多相バリアントとか、 GADTとかで型情報(型タグ?)を残さないといけないので、ダイレクトにunion typeを作るのは無理なんですよねと述べました。

参考:

しかしそれは本質的な問題ではなさそうな気もします。

私はいつもHaskellで以下のような直和型を書いて本当に良いのか悩み苦しんでいます。

data My = MyInt Int | MyText Text

これを書かないといけない理由は先程述べた通り型情報を残さないといけないからですが、別に、

union My = Int | Text

と書いたら言語ランタイム的には直和型のように動き、シンタックスは直和型のように動くシステムがあれば型がシンプルに見えて嬉しいのではないか? と思いました。

言語安全性的にはパターンマッチを使わないと内部にアクセス出来ないようにすれば問題はないわけですし。

暗黙的に変換出来ないとあまり便利ではない

しかし、その後の対話ですぐに、かなり厄介な問題があることに気が付きました。

union My = Int | Text
union Me = Int | Text

このコードがあった時、 MyMeは互いに暗黙的に変換可能にするべきか? と言う問題です。

もちろん暗黙的変換はダメですよね。

しかし、これらの変換をダメとすると、実用的につらい問題が発生します。

union My = Int | Text
f :: My -> Int

があった時、もちろんユーザは、

g :: Int
g = f 0

のようにIntを直接渡して使いたいと思います。しかし、暗黙的に変換しないので、 IntMyは違う型なのでエラーになります。

では変換する関数が用意されるようにすれば良いのかと言うと、それはMyIntコンストラクタを使って変換するのと何が違うのかと言う話になります。

私は最近以下のようなコードを書いてます。

class ToMy where
  toMy a -> My

instance ToMy Int where
  toMy = MyInt

instance ToMy Text where
  toMy = MyText

暗黙的変換を入れないとすると、コードの書き易さと言う点ではもうこれで良いですよね。 Genericとかでもうちょっとボイラープレート減らせないかとかは思いますが…

暗黙的に変換出来るルールを作るのが難しすぎる

では、 unionは単なる高級なtypedefに過ぎないことにして、暗黙的に構造的部分型の仕組みを使って変換すれば良いのかと言うと、それも厄介です。

実行時に豊富な型情報を取れるTypeScriptでも単なるオブジェクトの直和型などはどちらの型なのか判別できず、ユーザ定義のtype guardに頼らざるを得ないぐらいです。

これを実装するのはめっちゃ面倒な上、単純な型の組み合わせならともかく、型推論が複雑になってきたらどうするんだろとか仕様を決めるのが大変です。

例えばInt | Intとか出てきたらどうするんだ。

TypeScriptでも良く出てくる問題でそのためにfp-tsのEitherとかがあるわけですし。

open-unionあるじゃん

私には自然なHaskellへのunion typeへの導入は簡単には思いつきませんでした。

とまで書き連ねた所で、「普通にHaskellへのunion typeの実装あるんじゃね?」ということに気が付きました。

あるらしい。

これQiitaの記事にいいねしているから過去に見たことがあるようなんですよね。何故か完全に存在を忘れていました。

似非原さんにはその場で「Union型が独立して存在していたらunion typeよりは問題が少なくて良いのにね」って言われていたのに何故思い出さなかったんだろう…

これは暗黙的な変換を実現せず、変換には普通の型からはliftUnionを使って、 union同士の変換にはreUnionを使っているようですね。

型の見た目は同じような型コンストラクタを書かなくて良い分すっきりすると思います。

変換に一々liftUnionを指定するのがだるいことと、 typeが弱いtypedefでしかない問題と、 a | aどうするの問題は解決していないですが、これ使えば良いのでは…?

一見パターンマッチの網羅性無くないかと思ってしまいましたが、 typesExhaustedの型は空しか受け付けないので網羅性必須にできますね。

まあ、今現在直面してる型名多すぎ事案では、単なる直和ではなく直積もたまに出るなどが起きそうですし、そもそもa | aやたまたま同じ型構造を持つ違う意味のunionが発生しそうです。レイヤー分けが構造を整理することにもなります。

しかし今度はopen-unionも選択肢の一つとして記憶しておくことにします。

2021-10-09追記: open-unionは今の環境だとコンパイルできない

ちょっと前にopen-unionをプロダクトに試してみようと思いましたが、 GHCのバージョンなのか、 Stackage LTSの他のライブラリの問題なのか知りませんが、使うことが出来ませんでした。

思い出してみるとScala 3にはunion typeがあった

先日リリースされたScala 3(Dotty)にはunion typeがあります。

Union Types | Scala 3 — Book | Scala Documentation

これは暗黙的な変換を実装して、型は無名で実装しているようですね。

構造を把握してunion typed同士でも変換が行われるようです。

Scala 3: Intersection and Union Types | by Dean Wampler | Scala 3 | Medium

Scala 3の公式ドキュメントに書かれている通り、あくまで簡易的に型を表現するための選択肢の一つとしてunion typeがあるのは良いのかもしれません。

2021-10-09追記: union typeやっぱりあった方が良いですよね

多少複雑になりますし、問題を解決するのも難しいですが、大量のSum Typeが溢れかえるのを考えるとunion typeはやっぱりあった方が便利ですよね。同じ型とかで問題になることもありますが、サクッと書けて問題になった時だけを考えれば良い。

この記事書いた時はちょっとだけすっぱい葡萄になっていた気がします。