← melang

Enum / Sum Type

Last Updated: 2026-09-28 02:00

結論

melangでは、enumを整数定数へ寄せず、独立した型として持つ。variantは単なる整数・文字列定数ではなく、その enum 型の値そのものである。フィールド構成を持ちうる型に近い存在であるため、melangの型命名規則(型はPascalCase、変数・関数はlowerCamelCase)に合わせ、variant名もPascalCaseで始める。

enum Status {
    Pending
    Running
    Completed
}

enumと整数・文字列の暗黙変換は禁止する。外部表現との変換は明示的に行い、不正な値から enum を生成する処理はResultを返す。

matchは網羅的であることを要求する

enumに対するmatchは、全variantを網羅することをコンパイル時に要求する。

match status {
    Status.Pending => "pending"
    Status.Running => "running"
    Status.Completed => "completed"
}

variantを追加したとき、既存のmatchに不足があればコンパイルエラーとして検出できることを重視する。実行するまで気づけない分岐漏れをなくすのが狙いである。

Rust方式のenum / ADTAlgebraic Data Type(代数的データ型)の略。複数の型を組み合わせて新しい型を作る手法で、「AまたはB」のように取りうる形が閉じた集合になっているものをSum Type(直和型)と呼ぶ。Rustの`enum`やHaskellの`data`が代表例で、各variant(構成要素)が異なるフィールドを持てる点で、単純な整数の列挙とは異なる。を基本モデルにする

単純な列挙ではなく、Rustのenumを基本モデルとして採用する。各variantはそれぞれ異なる型・異なるフィールド構成を持てる。

enum Result {
    Success {
        user: User
    }
    ValidationError {
        errors: List<String>
    }
    NetworkError {
        status: Int
        message: String
    }
}

variantはフィールドなし・単一値・複数の名前付きフィールドを持てる設計とする。Option / Resultもこの仕組みで表現できる形を基本とする。Java/Kotlinの sealed class/interface のように「別々に定義された型を閉じた継承階層にする」機能を追加するかは別論点とし、まずはRust型enum / ADTで必要なモデリングを賄う。

Boolean も閉じた集合として扱う

Booleanは言語上の基本型として提供するが、意味論としてはtrue / falseの閉じた2値集合として扱う。matchではenumと同様に網羅性を検査できる。

任意の値へのmatchとの違い

enum / Booleanのmatchは値集合が閉じているため、全ケースを列挙できればそのまま値を返せる。一方、整数・文字列など任意の値へのmatchは値集合を網羅できない。melangでは値matchを許可する場合、未一致を型として扱えるよう結果をResultとする方針とする。

このnoteで決めないこと

他言語比較

enum / Sum Typeの設計は、大きく3つに分かれる。

言語enum / 相当機能payload付きvariant網羅性チェック説明
Rustenum○○
MoonBitenum○○
Swiftenum(associated value)○○
Kotlinenum class / sealed classsealedで○○
Javaenum / sealed interface限定的条件付き
Scalaenum / sealed ADT○○(条件あり)
C#enum(整数型)×基本なし
C++enum / enum class×基本なし
Cenum(整数定数)××
Go組み込みenumなし(named type + const)××
Zigunion(enum)(tagged union)○○
OCamlvariant type○○(警告)
F#判別共用体(Discriminated Union)○○(警告)
Haskelldata○compiler warning等
Erlang専用enum型なし(tagged tuple)○pattern matching中心
Elixir専用enum型なし(tagged tuple)○pattern matching中心
TypeScriptenum / union typeunionで○強制なし(neverで検証可)
JavaScript組み込みenumなし××
PHPenum(backed enum)×matchは実行時エラー
Ruby組み込みenumなし××
Pythonenum.Enum基本×基本なし

比較の観点は次の通りである。

  1. 言語組み込みのenum(相当機能)を持つか。
  2. variantごとに異なるフィールド構成(payload)を持てるか。
  3. matchやswitchが全variantを網羅しているかを、コンパイラが検査するか。

Go・C++・C#は、enum専用の型機構を持たないか、整数値の列挙にとどまる。Goは言語組み込みのenumがなく、named type + const / iotaで関連する整数定数を定義する。Go公式Wikiも「Go does not support enums」と明記している。C++のenum / enum classとC#のenumはunderlying integral typeを持つ列挙型で、payloadを持てない。いずれもswitchに網羅性チェックはなく、variant追加時の対応漏れをコンパイル時に検出できない。

Java・Kotlin・TypeScript・Pythonは、単純な列挙とADT表現を別の機構に分ける。Javaのenumはクラスとして状態・メソッドを持てるが、ADT用途はsealed interface + recordを組み合わせる。Kotlinも同様に、単純な列挙はenum class、payload付きのADTはsealed class / sealed interfaceが担う。どちらもswitch expressionやwhenとsealed階層を組み合わせれば網羅性を検査できる。TypeScriptのenumはGoに近い定数寄りの機能で、payload付きのADT表現にはliteral union(discriminated union)を使うのが実務上の主流であり、switch自体に網羅性チェックはないがnever型への代入という慣用句で擬似的に検証できる。PythonのbackedなENUM(enum.Enum)はpayloadを持たず、match文自体に網羅性チェックは基本的にない。PHPのbacked enumも文字列/整数値を持てるがpayloadは持てず、match式の網羅漏れはコンパイル時ではなく実行時のUnhandledMatchErrorとして現れる。

Rust・Swift・Scala・MoonBit・Zigは、enum自体がpayload付きのADTとして設計されている。Rustのenumは各variantが異なるフィールド構成を持てるdiscriminated unionで、matchは全variantの網羅をコンパイラが要求する。Swiftのenumはassociated valueを持て、switchも同様に全caseの網羅を要求する。Scala 3のenumもvariantごとに異なるフィールドを持てるADT表現に使え、sealedな階層へのmatchは網羅性チェックの対象になる。MoonBitのenumもconstructorがpayloadを持てるADT型で、matchは全constructorの網羅を要求する。Zigはenum単体では整数値ベースの単純な列挙にとどまるが、enumをタグとして持つunionを組み合わせたunion(enum)がpayload付きのvariantを安全に追跡するtagged unionとして機能し、switchは全variantの網羅を要求する。これらはmelangが目指すモデルに近い。

Haskell・OCaml・Elixirは、専用のenum型を持たないが、pattern matchingを中核にしたADT表現を持つ。Haskellのdata、OCamlのvariant typeは、コンストラクタごとに異なるデータを持てる代数的データ型で、パターンマッチの網羅漏れはコンパイラの警告として検出できる(Haskellは-Wincomplete-patterns、OCamlはデフォルトの警告)。Elixirには専用のenum型がなく、atomとtagged tupleによるpattern matchingでADT的に表現するが、網羅性はコンパイラではなく実行時のマッチ失敗(FunctionClauseError)として現れる。JavaScript・Rubyには組み込みのenumがなく、オブジェクトやsymbolで慣用的に代替するが、網羅性を検査する仕組み自体がない。

トレードオフ

enum専用の型機構を持たない設計(Go, C++, C#など)

単純な列挙とADTを別機構で用意する設計(Java, Kotlin, TypeScript, Pythonなど)

enum自体をpayload付きのADTとして設計する方式(Rust, Swift, Scala, MoonBit, Zig)

専用のenum型を持たずpattern matching + data/variant typeで表現する方式(Haskell, OCaml, Elixir)

melangの選択

melangは、Rust・Swift・MoonBit・Zigと同じく、enum自体をpayload付きのADTとして設計し、matchの網羅性チェックをコンパイラに強制させる方式を選ぶ。

Go方式の軽量さよりも、列挙可能な状態集合を型システムが把握できることと、パターンマッチの網羅性検査を優先する。