trait の derive#

MoonBit では、型定義からいくつかの組み込み trait を自動的に derive できます。

trait T を derive するには、その型で使われているすべてのフィールドが T を実装している必要があります。例えば、struct A { x: T1; y: T2 } に対して Show を derive するには、T1: ShowT2: Show の両方が必要です。

Eq と Compare#

derive(Eq)derive(Compare) は、それぞれ等価性と比較のための対応するメソッドを生成します。フィールドは定義順に比較されます。enum では、ケース同士の順序は定義順になります。

struct DeriveEqCompare {
  x : Int
  y : Int
} derive(Eq, Compare)

test "derive eq_compare struct" {
  let p1 = DeriveEqCompare::{ x: 1, y: 2 }
  let p2 = DeriveEqCompare::{ x: 2, y: 1 }
  let p3 = DeriveEqCompare::{ x: 1, y: 2 }
  let p4 = DeriveEqCompare::{ x: 1, y: 3 }

  // Eq
  assert_eq(p1 == p2, false)
  assert_eq(p1 == p3, true)
  assert_eq(p1 == p4, false)
  assert_eq(p1 != p2, true)
  assert_eq(p1 != p3, false)
  assert_eq(p1 != p4, true)

  // Compare
  assert_eq(p1 < p2, true)
  assert_eq(p1 < p3, false)
  assert_eq(p1 < p4, true)
  assert_eq(p1 > p2, false)
  assert_eq(p1 > p3, false)
  assert_eq(p1 > p4, false)
  assert_eq(p1 <= p2, true)
  assert_eq(p1 >= p2, false)
}
enum DeriveEqCompareEnum {
  Case1(Int)
  Case2(label~ : String)
  Case3
} derive(Eq, Compare)

test "derive eq_compare enum" {
  let p1 = DeriveEqCompareEnum::Case1(42)
  let p2 = DeriveEqCompareEnum::Case1(43)
  let p3 = DeriveEqCompareEnum::Case1(42)
  let p4 = DeriveEqCompareEnum::Case2(label="hello")
  let p5 = DeriveEqCompareEnum::Case2(label="world")
  let p6 = DeriveEqCompareEnum::Case2(label="hello")
  let p7 = DeriveEqCompareEnum::Case3

  // Eq
  assert_eq(p1 == p2, false)
  assert_eq(p1 == p3, true)
  assert_eq(p1 == p4, false)
  assert_eq(p1 != p2, true)
  assert_eq(p1 != p3, false)
  assert_eq(p1 != p4, true)

  // Compare
  assert_eq(p1 < p2, true) // 42 < 43
  assert_eq(p1 < p3, false)
  assert_eq(p1 < p4, true) // Case1 < Case2
  assert_eq(p4 < p5, true)
  assert_eq(p4 < p6, false)
  assert_eq(p4 < p7, true) // Case2 < Case3
}

Debug#

derive(Debug) は、その型に対する構造的なデバッグ実装を生成します。これはテストで debug_inspect と一緒に使ったり、診断メッセージを整形するときに @debug.to_string と一緒に使ったりできます。

struct DebugPoint {
  x : Int
  y : Int
} derive(Debug)

test "derive debug struct" {
  let point = DebugPoint::{ x: 1, y: 2 }
  debug_inspect(point, content="{ x: 1, y: 2 }")
}

列挙型も Debug を derive できます:

enum DebugShape {
  Circle(radius~ : Int)
  Rect(width~ : Int, height~ : Int)
} derive(Debug)

test "derive debug enum" {
  let shape = DebugShape::Rect(width=3, height=4)
  debug_inspect(shape, content="Rect(width=3, height=4)")
}

Default#

derive(Default) は、その型のデフォルト値を返すメソッドを生成します。

struct では、すべてのフィールドをデフォルト値にした struct がデフォルト値になります。

struct DeriveDefault {
  x : Int
  y : String?
} derive(Default, Eq)

test "derive default struct" {
  let p = DeriveDefault::default()
  assert_true(p == DeriveDefault::{ x: 0, y: None })
}

enum では、パラメータを持たない唯一のケースがデフォルト値になります。

enum DeriveDefaultEnum {
  Case1(Int)
  Case2(label~ : String)
  Case3
} derive(Default, Eq)

test "derive default enum" {
  assert_true(DeriveDefaultEnum::default() == DeriveDefaultEnum::Case3)
}

ケースがない enum や、パラメータなしケースが複数ある enum は Default を derive できません。

enum CannotDerive1 {
    Case1(String)
    Case2(Int)
} derive(Default) // cannot find a constant constructor as default

enum CannotDerive2 {
    Case1
    Case2
} derive(Default) // Case1 and Case2 are both candidates as default constructor

Hash#

derive(Hash) はその型の hash 実装を生成します。これにより、HashMapHashSet など、Hash 実装を期待する場所でその型を使えるようになります。

struct DeriveHash {
  x : Int
  y : String?
} derive(Hash, Eq)

test "derive hash struct" {
  let hs = @hashset.HashSet([])
  hs.add(DeriveHash::{ x: 123, y: None })
  hs.add(DeriveHash::{ x: 123, y: None })
  @test.assert_eq(hs.length(), 1)
  hs.add(DeriveHash::{ x: 123, y: Some("456") })
  @test.assert_eq(hs.length(), 2)
}

Arbitrary#

derive(Arbitrary) は指定した型のランダム値を生成します。

FromJson と ToJson#

derive(FromJson)derive(ToJson) は、型を JSON へ/から変換する往復可能なメソッド実装を自動的に derive します。主な用途はデバッグや、人間が読める形での保存です。

struct JsonTest1 {
  x : Int
  y : Int
} derive(FromJson, ToJson, Eq)

enum JsonTest2 {
  A(x~ : Int)
  B(x~ : Int, y~ : Int)
} derive(FromJson(style="legacy"), ToJson(style="legacy"), Eq)

test "json basic" {
  let input = JsonTest1::{ x: 123, y: 456 }
  let expected : Json = { "x": 123, "y": 456 }
  @test.assert_eq(input.to_json(), expected)
  assert_true(@json.from_json(expected) == input)
  let input = JsonTest2::A(x=123)
  let expected : Json = { "$tag": "A", "x": 123 }
  @test.assert_eq(input.to_json(), expected)
  assert_true(@json.from_json(expected) == input)
}

どちらの derive 指示子も、シリアライズとデシリアライズの具体的な振る舞いを設定するための複数の引数を受け取れます。

警告

JSON シリアライズ引数の実際の振る舞いは不安定です。

警告

JSON derive 引数は、生成される形式を大まかに制御するためのものです。型のレイアウトを厳密に制御したいなら、代わりに 2 つの trait を直接実装することを検討してください

最近、多数の高度なレイアウト調整引数を非推奨にしました。そうした用途や今後それらを使う場合は、trait を手動実装してください。引数には reprcase_reprdefaultrename_all などがあります。

struct JsonTest3 {
  x : Int
  y : Int
} derive (
  FromJson(fields(x(rename="renamedX"))),
  ToJson(fields(x(rename="renamedX"))),
  Eq,
)

enum JsonTest4 {
  A(x~ : Int)
  B(x~ : Int, y~ : Int)
} derive(FromJson, ToJson, Eq)

test "json args" {
  let input = JsonTest3::{ x: 123, y: 456 }
  let expected : Json = { "renamedX": 123, "y": 456 }
  @test.assert_eq(input.to_json(), expected)
  assert_true(@json.from_json(expected) == input)
  let input = JsonTest4::A(x=123)
  let expected : Json = ["A", { "x": 123 }]
  @test.assert_eq(input.to_json(), expected)
  assert_true(@json.from_json(expected) == input)
}

enum のスタイル#

現在、enum のシリアライズには legacyflat の 2 種類のスタイルがあり、style 引数でどちらかを選ぶ必要があります。次の enum 定義を考えます:

enum E {
  One
  Uniform(Int)
  Axes(x~: Int, y~: Int)
}

derive(ToJson(style="legacy")) では、enum は次のように出力されます:

E::One              => { "$tag": "One" }
E::Uniform(2)       => { "$tag": "Uniform", "0": 2 }
E::Axes(x=-1, y=1)  => { "$tag": "Axes", "x": -1, "y": 1 }

derive(ToJson(style="flat")) では、enum は次のように出力されます:

E::One              => "One"
E::Uniform(2)       => [ "Uniform", 2 ]
E::Axes(x=-1, y=1)  => [ "Axes", -1, 1 ]

Option の derive#

例外として、組み込み型 Option[T] があります。本来なら T | undefined と解釈したいところですが、Option[Option[T]] に対して Some(None)None を区別できなくなるという問題があります。

そのため、struct の直接フィールドである場合に限って T | undefined と解釈し、それ以外では [T] | null と解釈されます:

struct A {
  x : Int?
  y : Int??
  z : (Int?, Int??)
} derive(ToJson)

test {
  json_inspect({ x: None, y: None, z: (None, None) }, content={
    "z": [null, null],
  })
  json_inspect({ x: Some(1), y: Some(None), z: (Some(1), Some(None)) }, content={
    "x": 1,
    "y": null,
    "z": [[1], [null]],
  })
  json_inspect({ x: Some(1), y: Some(Some(1)), z: (Some(1), Some(Some(1))) }, content={
    "x": 1,
    "y": [1],
    "z": [[1], [[1]]],
  })
}

コンテナ引数#

  • rename_fieldsrename_cases(enum のみ)は、フィールド(enum と struct)および enum ケースを指定した形式へ一括でリネームします。利用できる形式は次のとおりです:

    • lowercase

    • UPPERCASE

    • camelCase

    • PascalCase

    • snake_case

    • SCREAMING_SNAKE_CASE

    • kebab-case

    • SCREAMING-KEBAB-CASE

    例:my_long_field_name というフィールドに対して rename_fields = "PascalCase" を指定すると、MyLongFieldName になります。

    リネームは、フィールド名が snake_case、struct/enum ケース名が PascalCase であることを前提にしています。

  • cases(...)(enum のみ)は enum ケースのレイアウトを制御します。

    警告

    これは将来、case アトリビュートに置き換えられる可能性があります。

    例えば、次の enum では

    enum E {
      A(...)
      B(...)
    }
    

    cases(A(...), B(...)) を使って各ケースを制御できます。

    詳細は以下の Case arguments を参照してください。

  • fields(...)(struct のみ)は struct フィールドのレイアウトを制御します。

    警告

    これは将来、field アトリビュートに置き換えられる可能性があります。

    例えば、次の struct では

    struct S {
      x: Int
      y: Int
    }
    

    fields(x(...), y(...)) を使って各フィールドを制御できます。

    詳細は以下の Field arguments を参照してください。

Case 引数#

  • rename = "..." はこの特定の case の名前を変更し、既存のコンテナ全体のリネーム指定があればそれを上書きします。

  • fields(...) はこの case のペイロードのレイアウトを制御します。なお、位置引数フィールドのリネームは現在できません。

    詳細は以下の Field arguments を参照してください。

Field 引数#

  • rename = "..." はこの特定の field の名前を変更し、既存のコンテナ全体のリネーム指定があればそれを上書きします。