| 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 することができます。
// 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)
}
}- 既存の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型を実装するかどうかも考慮する必要があります。
moonbitlang/moonbit-evolution#13
私の TypeScript の経験では、プリミティブな Union 型は推論コストが高く、特にサブタイプと組み合わせると(それを実装するかはさておき)エラーの可読性が非常に低くなります。
Rust の enum は明瞭ですが、低レベルの表現に言語固有のABIを要求します。そのため、wasm_bindgen や他のFFIでは const enum に出力が制限されています