nikkie-ftnextの日記

イベントレポートや読書メモを発信

タグ付きユニオン(Tagged Unions)をさがして

はじめに

君を退屈から救いに来たんだ♪ nikkieです。

YAPC::Hiroshima 2024で聞いた伊藤直也さんのトークから気づきを1本したためます。
タグ付きユニオン、完全に理解したと思う!

目次

(再演) 関数型プログラミングと型システムのメンタルモデル(YAPC::Hiroshima 2024)

気づきが多々ある素晴らしいトークでした1
その中から以下を取り上げます(このスライドは初演のものと思われます)。

直和(OR)を使うことで、仕様上あり得ない状態を表現せずに済んでいます!(Making illegal states unrepresentable)

型の直和という概念、すでにインプットしていた知識と結びつきました。

『プロを目指す人のためのTypeScript入門』

Forkwell Libraryより。

著者のうひょさんによる基調講演で、タグ付きユニオンなる概念を聞きました。

タグ付きユニオンとは

「タグ」を見れば判別できるユニオン型

以下のスライドは伊藤直也さんトークと同じですよね。

ロバストPython

ここまではTypeScriptの話ですが、Pythonにおいても同様の解説が存在します。
それが『ロバストPython』!(箇所は4.2です)

データクラスを例に、型の直積と直和を比較して説明されます。

直積で表す例:

# ref: https://github.com/pviafore/RobustPython/blob/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/chapter4/product_type.py
@dataclass
class Snack:
    name: str
    condiments: set[str]
    error_code: int
    disposed_of: bool

Snack("Hotdog", {"Mustard", "Ketchup"}, 5, False)

詳細は書籍を見ていただきたいのですが、取りうる値は

  • nameが3通り
  • condimentsが4通り
  • error_codeが6通り
  • disposed_ofが2通り

すなわち144通りです。

問題は、これらの状態が全部有効だとは限らないことだ。

「error_codeが0でない場合にdisposed_ofをTrueにしたい」といった制約があります。
直積で表すと、仕様上あり得ない状態を表現してしまっているということですね。

直和で表す例を見ましょう

# ref: https://github.com/pviafore/RobustPython/blob/dafb95d801dff2c8ff7856ba46d3c052d54e0033/code_examples/chapter4/sum_type.py
@dataclass 
class Error:
    error_code: int
    disposed_of: bool

@dataclass
class Snack:
    name: str
    condiments: set[str]
    
snack: Union[Snack, Error] = Snack("Hotdog", {"Mustard", "Ketchup"})

SnackかErrorのUnion!

snackはSnack(nameとcondimentsだけ)かError(エラーコードと論理値だけ)になる。

144あった取りうる値は、Snackが3*4で12通り、Errorが5*2で10通り。
合計22通りまで減りました!(お見事)

組合せ爆発していない! めっちゃ助かりますね🙌
Union[Snack, Error]という型ヒントは、きもち、Result型なるものにつながるようにも見えます。

Pydanticのドキュメント

Pydanticのドキュメントにも見つけました。
https://docs.pydantic.dev/2.6/concepts/unions/#discriminated-unions

Cat、Dog、Lizardの3つのクラスがあり、いずれかのインスタンスを持つ形でModelが定義されます

class Model(BaseModel):
    pet: Union[Cat, Dog, Lizard] = Field(..., discriminator='pet_type')
    n: int

Fielddiscriminatorがタグに使う属性名の指定2
ここではpet_type属性です。

伊藤さんの資料に戻ると「和で組み合わせて構築したものは、パターンマッチで分解」とあります。

Pydanticのドキュメントのコードを元に手を動かします。
Python 3.12.0、Pydantic 2.6.1)

タグ付きユニオン、パターンマッチでいい感じに書けた気がする!3

終わりに

YAPC::Hiroshimaの伊藤さんトークをきっかけに、『プロを目指す人のためのTypeScript入門』『ロバストPython』の解説がつながり、タグ付きユニオンを完全に理解しました。

  • 型の直和(Union)により、仕様上あり得ない状態を表現せずに済む(組合せ爆発しない)
  • タグを見れば判別できる直和が、タグ付きユニオン
    • パターンマッチで分解

思うに、直積だと少ない行数で書けるので、魅力的に映ります。
しかしながら、少ない行数ゆえに表現力には限りがあるため、コードを増やした直和でむしろ表現したほうが、長期的に恩恵を受けられるととらえました。

P.S. ちょうぜつ本のコラムより

Tagged Unionsという語はちょうぜつ本で知りました(8章のコラム)。
PHPにTagged Unionsの提案(RFC)がされています(StatusはDraft)

P.S. やめ太郎さんのこの記事もタグ付きユニオンのことやないか!

プロパティとして型名を持たせてあげるの

実装は、パターンマッチで型名による分岐をしていますね(『プロを目指す人のためのTypeScript入門』を参照した記事です)


このエントリの元ツイートです。


  1. タグ付きユニオン完全理解のほか、「型は集合」が飲み込めました
  2. https://docs.pydantic.dev/2.6/concepts/fields/#discriminator
  3. ロバストPython』のPydanticを使わない実装も、パターンマッチで分解すればよいと考えています