Deriving
Deriving provides a way to automatically generate interface implementations for class, struct and enum types based on their fields and properties.
Currently, only the particular list of interface is supported: ToString, Hashable,
Equatable and Comparable.
To use deriving apply @Derive macro on a type:
import std.deriving.*
@Derive[ToString]
class User {
User(
let name: String,
let id: Int
) {}
}
main() {
println(User("root", 0)) // -> "User(name = root, id = 0)"
}
During generation, Deriving does collect fields to be used, basic var and let fields, including those which specified in a primary constructor. When applied to a enum, enum constructor parameters are considered instead. Static fields and properties can't be used. Also fields shouldn't be private for deriving to work.
The collected fields are used for deriving and their types need to implement the target interface so that the field results are combined together. For example, when we are deriving ToString, the generated code will invoke toString on all fields and then concatenate the results into a single string with the field names. If one of the fields has incompatible type, then an error is reported and deriving fails to complete.
Also note that a class marked for deriving should be final: it shouldn't be open, abstract or sealed.
Some fields could have special meaning and it makes no big sense to consider their values.
Such fields can be excluded by applying @DeriveExclude on them:
class User {
User(let name: String) {}
@DeriveExclude
let lazyHashCode = 0 // it will not be printed because it's excluded
}
By default only fields are considered while properties can be explicitly enabled
via @DeriveInclude:
@Derive[ToString]
class User {
User(
let id: Int
) {}
@DeriveInclude
prop name: String {
get() { passwdFile.getUserName(id) }
}
}
main() {
println(User(0)) // -> "User(id = 0, name = root)"
}
Please note that static fields and properties can't be included/excluded because they are always ignored.
Notice that the order has changed because the property name is declared after id.
To change the order of fields use @DeriveOrder:
@Derive[ToString]
@DeriveOrder(name, id)
class User {
User(
let id: Int
) {}
@DeriveInclude
prop name: String {
get() { passwdFile.getUserName(id) }
}
}
main() {
println(User(0)) // -> "User(name = root, id = 0)"
}
General Derive macro syntax
Macro @Derive accepts a comma-separated list of interface names. Also the macro
can be repeated multiple times but all @Derive macro invocations should be on
the top while other options macro like @DeriveOrder should always follow.
The order of interface names doesn't make any difference.
@Derive[ToString, Hashable]
@Derive[Equatable]
@DeriveOrder[currency,price,quantity]
struct Order {}
When deriving several interfaces that are intersecting, for example, Comparable
also includes Equatable, then either both or the most specific can be applied:
@Derive[Comparable] // does also generate Equatable
This also works:
@Derive[Comparable, Equatable]
Include and exclude fields
All fields are considered by default including those that are defined as primary
constructor parameters. To explicitly exclude a fields from processing, one can
apply @DeriveExclude on it:
@Derive[ToString]
struct S {
S(let id) { key = "s_${id}" }
@DeriveExclude
let key: String
}
Properties are never considered by default and need to be included via @DeriveInclude
@Derive[ToString]
struct S {
S(let id) {}
@DeriveInclude
prop key: String {
get() { "s_${id}" }
}
}
Both fields and properties can't be private when invoked in deriving. So private
fields should be either excluded or made package-private
(private modified should be removed).
Supported interfaces
The following interfaces are supported for deriving:
ToStringHashableEquatableComparable
Custom interfaces can't be plugged at the moment.
Change order
For sorting and comparing instances of complex types consisting of multiple fields,
the order in which the fields are tested is often important. By default all the fields
are considered in the order of declaration. To amend the order there is @DeriveOrder macro.
@Derive[Comparable, ToString]
struct Floor {
Floor(
let level: Int,
let building: Int
) {}
}
main() {
let floors = [
Floor(1, 2),
Floor(3, 2),
Floor(2, 1)
]
floors.sort()
for (f in floors) { println(f) }
}
The example above prints the following, which probably makes no big sense
Floor(level = 1, building = 2)
Floor(level = 2, building = 1)
Floor(level = 3, building = 2)
However if we apply another order the result will be different
@Derive[Comparable, ToString]
@DeriveOrder[building, level]
struct Floor {}
The result will be sorted by building number first and then by level number:
Floor(building = 1, level = 2)
Floor(building = 2, level = 1)
Floor(building = 2, level = 3)
Generics
Implementing interfaces for generic types usually requires applying constraints so that a type only implements an interface under some condition. Consider the following example:
class Cell<T> {
Cell(let value: T) {}
}
Here we probably would like to be able to print a cell only when it's value is printable. To achieve it, we would write an extend with constraints:
extend <T> Cell<T> <: ToString where T <: ToString {
public func toString(): String {
"Cell(value = ${value})"
}
}
When we use deriving, it tries to apply constraints to all generic parameters by default, so the following does the same as the extend above:
@Derive[ToString]
class Cell<T> {
Cell(let value: T) {}
}
However there are cases when the default behaviour is not what we need. In these
cases it's possible to override default constraints using where inside of @Derive:
interface PrintableCellValue <: ToString { ... }
@Derive[ToString where T <: PrintableCellValue]
class Cell<T> {}
Please note that in the example above the custom constraints are only applied for
ToString so if need to be changed for all interfaces it should be repeated for
every interface in separate.
@Derive[ToString where T <: PrintableCellValue]
@Derive[Hashable where T <: PrintableCellValue & Hashable]
class Cell<T> {}
Performance considerations
Since Deriving is based on Cangjie macro and doesn't involve any reflection, the runtime performance of derived implementations is comparable to handwritten. However, deriving involves code transformation at compile-time so it obviously affects compilation time rather than runtime performance.