Skip to content

Instantly share code, notes, and snippets.

@mizchi
Last active November 2, 2025 14:54
Show Gist options
  • Select an option

  • Save mizchi/f836a61f6a2c23309b18ddcdc86f226e to your computer and use it in GitHub Desktop.

Select an option

Save mizchi/f836a61f6a2c23309b18ddcdc86f226e to your computer and use it in GitHub Desktop.
Proposal Author Status Review and discussion
ME-0011
mizchi
Under review
Github issue

新しい json enum 構文を提案します。

モチベーション

現在、外部から受け取ったJSONのFFIを記述する際に、記述量が多くなってしまう問題があります。

特に、タグ付きユニオンの配列(TypeScriptにおけるArray<A | B | C | ...>)は FromJson で自動で導出できないために、安全でない動的アサーションの記述量が多くなります。

例として、 openai のレスポンスを書こうとすると、コードのほとんどがユニオン型のキャストになります。

https://mooncakes.io/docs/tonyfettes/openai

とはいえ、実際に何度もFFIを記述しているとほぼパターン化が可能なこともわかってきます。

Moonbit はビルトインの Json 型と、そのパターンマッチを備えています。この文法を拡張することで、動的な処理なしに JSONに値をマップすることができる json enum 型を提案します。

提案

json enum <EnumName> {
  <Variant1>(<JsonMatcher>)
  <Variant2>(<JsonMatcher>)
  ..
}
// new syntax: json enum
json enum CounterAction {
  // Variant(JsonMatcher)
  Increment({
    "type": "increment",
    ..
  })
  Add({
    "type": "add",
    "payload": Number(_) as payload,
    ..
  })
  Other(Json)
} derive(FromJson)

Usage

// usage
let json: Json = [
  {
    "type": "increment"
  },
  {
    "type": "add",
    "payload": 3
  },
]
let actions: Array[CounterAction] = @json.from_json(json)

let mut value: Int = 0
for action in actions {
  match action {
    Increment(_) => value += 1
    Add({ amount }) => value += amount.to_int()
    Other(json) => ignore()
  }
}

また、 この json enum に対する js backend の .d.ts 型定義も一意にすることができます。

// generated .d.ts
export type CounterAction =
  | {
    "type": "increment"
  }
  | {
    "type": "add",
    "payload": number
  }
  | unknown

(現在の js backend の enum 出力は {_0: "add", _1: 3} のように、プロパティ名が消えてしまうので、ほぼ無意味です)

desugar

上記のコードは次のように desugar することができます。

// equivalant to
struct Increment {}
impl FromJson for Increment with from_json(self) {
  ...
}
struct Add {
  payload: Double
}
impl FromJson for Increment with from_json(self) {
  ...
}
enum CounterAction {
  Increment(Increment)
  Add(Add)
  Other(Json)
}
impl FromJson for CounterAction with from_json(self) {
  match self {
    { "type": "increment", .. } => Increment({ })
    { "type": "add", "payload": Number(_) as payload, .. } => Add({ payload })
    v => Other(v)
  }
}

Why json matcher?

  • 既存のJsonパターンマッチのセマンティクスを使うので、学習コストを抑えることができます
  • ビルトインの Json に対する拡張なので、バックエンドに依存しません
  • Desugar として実装できるので、実装コストが抑えられるかもしれません
  • 特に有用な側面として、Moonbitの予約語の衝突を避けることができます
    • type が衝突しがちです

懸念点

as が後置の構文であるため、どのプロパティが最終に束縛されるのか、目視しづらい可能性があります。ただ、この問題は guard opt is Some(v) else {...} 等にある同様の問題です。

この構文はタグ付きUnionのJSONのパースが目的ですが、これだけでは用途が少なすぎるかもしれません。

json enum があるなら、 json struct も存在するのが自然に思えます。

json struct Add {
  "type": "increment" as tag,
  "payload": Number(_) as payload
}

Union 型に対する意見

これを実装すべきかどうかを考えるなら、将来的にUnion型を実装するかどうかも考慮する必要があります。

moonbitlang/moonbit-evolution#13

私の TypeScript の経験では、プリミティブな Union 型は推論コストが高く、特にサブタイプと組み合わせると(それを実装するかはさておき)エラーの可読性が非常に低くなります。

Rust の enum は明瞭ですが、低レベルの表現に言語固有のABIを要求します。そのため、wasm_bindgen や他のFFIでは const enum に出力が制限されています

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment