Deriving traits#
MoonBit supports deriving a number of builtin traits automatically from the type definition.
To derive a trait T
, it is required that all fields used in the type implements T
.
For example, deriving Show
for a struct struct A { x: T1; y: T2 }
requires both T1: Show
and T2: Show
Show#
derive(Show)
will generate a pretty-printing method for the type.
The derived format is similar to how the type can be constructed in code.
struct MyStruct {
x : Int
y : Int
} derive(Show)
test "derive show struct" {
let p = MyStruct::{ x: 1, y: 2 }
assert_eq!(Show::to_string(p), "{x: 1, y: 2}")
}
enum MyEnum {
Case1(Int)
Case2(label~ : String)
Case3
} derive(Show)
test "derive show enum" {
assert_eq!(Show::to_string(MyEnum::Case1(42)), "Case1(42)")
assert_eq!(Show::to_string(MyEnum::Case2(label="hello")), "Case2(label=\"hello\")")
assert_eq!(Show::to_string(MyEnum::Case3), "Case3")
}
Eq and Compare#
derive(Eq)
and derive(Compare)
will generate the corresponding method for testing equality and comparison.
Fields are compared in the same order as their definitions.
For enums, the order between cases ascends in the order of definition.
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
}
Default#
derive(Default)
will generate a method that returns the default value of the type.
For structs, the default value is the struct with all fields set as their default value.
struct DeriveDefault {
x : Int
y : Option[String]
} derive(Default, Eq, Show)
test "derive default struct" {
let p = DeriveDefault::default()
assert_eq!(p, DeriveDefault::{ x: 0, y: None })
}
For enums, the default value is the only case that has no parameters.
enum DeriveDefaultEnum {
Case1(Int)
Case2(label~ : String)
Case3
} derive(Default, Eq, Show)
test "derive default enum" {
assert_eq!(DeriveDefaultEnum::default(), DeriveDefaultEnum::Case3)
}
Enums that has no cases or more than one cases without parameters cannot derive Default
.
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)
will generate a hash implementation for the type.
This will allow the type to be used in places that expects a Hash
implementation,
for example HashMap
s and HashSet
s.
struct DeriveHash {
x : Int
y : Option[String]
} derive(Hash, Eq, Show)
test "derive hash struct" {
let hs = @hashset.new()
hs.add(DeriveHash::{x: 123, y: None})
hs.add(DeriveHash::{x: 123, y: None})
assert_eq!(hs.size(), 1)
hs.add(DeriveHash::{x: 123, y: Some("456")})
assert_eq!(hs.size(), 2)
}
Arbitrary#
derive(Arbitrary)
will generate random values of the given type.
FromJson and ToJson#
derive(FromJson)
and derive(ToJson)
will generate methods that deserializes/serializes the given type from/to
JSON files correspondingly.
struct JsonTest1 {
x: Int
y: Int
} derive(FromJson, ToJson, Eq, Show)
enum JsonTest2 {
A(x~: Int)
B(x~: Int, y~: Int)
} derive(FromJson, ToJson, Eq, Show)
test "json basic"{
let input = JsonTest1::{ x: 123, y: 456 }
let expected: Json = { "x": 123, "y": 456 }
assert_eq!(input.to_json(), expected)
assert_eq!(@json.from_json!(expected), input)
let input = JsonTest2::A(x=123)
let expected: Json = { "$tag": "A", "x": 123 }
assert_eq!(input.to_json(), expected)
assert_eq!(@json.from_json!(expected), input)
}
Both derive directives accept a number of arguments to configure the exact behavior of serialization and deserialization.
Warning
The actual behavior of JSON serialization arguments is unstable.
struct JsonTest3 {
x: Int
y: Int
} derive(
FromJson(fields(x(rename = "renamedX"))),
ToJson(fields(x(rename = "renamedX"))),
Eq, Show
)
enum JsonTest4 {
A(x~: Int)
B(x~: Int, y~: Int)
} derive(
FromJson(rename_fields = "SCREAMING_SNAKE_CASE", repr(ext_tagged)),
ToJson(rename_fields = "SCREAMING_SNAKE_CASE", repr(ext_tagged)),
Eq, Show
)
test "json args"{
let input = JsonTest3::{ x: 123, y: 456 }
let expected: Json = { "renamedX": 123, "y": 456 }
assert_eq!(input.to_json(), expected)
assert_eq!(@json.from_json!(expected), input)
let input = JsonTest4::A(x=123)
let expected: Json = { "A": { "X": 123 } }
assert_eq!(input.to_json(), expected)
assert_eq!(@json.from_json!(expected), input)
}
Enum representations#
Enums can be represented in JSON in a number of styles. There are two aspects of the representation:
Tag position determines where the name of the enum tag (i.e. case or constructor name) is stored.
Case representation determines how to represent the payload of the enum.
Let’s consider the following enum definition:
enum E {
Uniform(Int)
Axes(x~: Int, y~: Int)
}
For tag position, there are 4 variants:
Internally tagged puts the tag alongside the payload values:
{ "$tag": "Uniform", "0": 1 }
,{ "$tag": "Axes", "x": 2, "y": 3 }
Externally tagged puts the tag as the JSON object key outside the payload values:
{ "Uniform": { "0": 1 } }
,{ "Axes": { "x": 2, "y": 3 } }
Adjacently tagged puts the tag payload in two adjacent keys in a JSON object:
{ "t": "Uniform", "c": { "0": 1 } }
,{ "t": "Axes", "c": { "x": 2, "y": 3 } }
Untagged has no explicit tag identifying which case the data is:
{ "0": 1 }
,{ "x": 2, "y": 3 }
.The JSON deserializer will try to deserialize each case in order and return the first one succeeding.
For case representation, there are 2 variants:
Object-like representation serializes enum payloads into a JSON object, whose key is either the tag name or the string of the positional index within the struct.
{ "0": 1 }
,{ "x": 2, "y": 3 }
Tuple-like representation serializes enum payloads into a tuple (jSON array), in the same order as the type declaration. Labels are omitted in tuple-like representations.
[1]
,[2, 3]
The two aspects can be combined freely, except one case: internally tagged enums cannot use tuple-like representation.
Container arguments#
repr(...)
configures the representation of the container. This controls the tag position of enums. For structs, the tag is assumed to be the type of the type.There are 4 representations available for selection:
repr(tag = "tag")
– Use internally tagged representation, with the tag’s object key name as specified.repr(untagged)
– Use untagged representation.repr(ext_tagged)
– Use externally tagged representation.repr(tag = "tag", contents = "contents")
– Use adjacently tagged representation, with the tag and contents key names as specified.
The default representation for struct is
repr(untagged)
.The default representation for enums is
repr(tag = "$tag")
case_repr(...)
(enum only) configures the case representation of the container. This option is only available on enums.case_repr(struct)
– Use struct-like representation of enums.case_repr(tuple)
– Use tuple-like representation of enums.
rename_fields
,rename_cases
(enum only),rename_struct
(struct only),rename_all
renames fields, case names, struct name and all names correspondingly, into a specific style.Available parameters are:
lowercase
UPPERCASE
camelCase
PascalCase
snake_case
SCREAMING_SNAKE_CASE
kebab-case
SCREAMING-KEBAB-CASE
Example:
rename_fields = "PascalCase"
for a field namedmy_long_field_name
results inMyLongFieldName
.Renaming assumes the name of fields in
snake_case
and the name of structs/enum cases inPascalCase
.cases(...)
(enum only) controls the layout of enum cases.For example, for an enum
enum E { A(...) B(...) }
you are able to control each case using
cases(A(...), B(...))
.See Case arguments below for details.
fields(...)
(struct only) controls the layout of struct fields.For example, for a struct
struct S { x: Int y: Int }
you are able to control each field using
fields(x(...), y(...))
See Field arguments below for details.
Case arguments#
rename = "..."
renames this specific case, overriding existing container-wide rename directive if any.fields(...)
controls the layout of the payload of this case. Note that renaming positional fields are not possible currently.See Field arguments below for details.
Field arguments#
rename = "..."
renames this specific field, overriding existing container-wide rename directives if any.