-
Notifications
You must be signed in to change notification settings - Fork 7
Code Generation
TODO
For the person
example, PIG generates code that looks like the example below. This class is
a persistent data structure
and therefore cannot be directly modified. Functions provided to manipulate instances of this class (withMeta
and copy
) always preserve the original version and return modified copies.
/** The Kotlin implementation of the type domain named `sample_type_domain`. */
class SampleTypeDomain private constructor() {
// ... other stuff
/**
* The Kotlin implementation of the `person` `product` type.
*/
class Person(
val first: SymbolPrimitive,
val last: SymbolPrimitive,
val children: List<Person>,
override val metas: MetaContainer
) : SampleTypeDomainNode() {
/** Converts person to its Ion s-expression representation. */
override fun toIonElement(): SexpElement {
/* ... */
}
/** Creates a copy of [Person] node that is identical to the original but includes the specified key and meta. */
override fun withMeta(metaKey: String, metaValue: Any): Person {
/* ... */
}
/** Creates a copy of [Person], replacing the existing metas collection. */
override fun copy(metas: MetaContainer): Person {
/* ... */
}
/**
* Creates a copy of [Person] with new values for the specified properties.
*
* Each parameter corresponds to a property on this class. If left unspecified, the copy will have
* the same value as the original.
*/
fun copy(
first: SymbolPrimitive = this.first,
last: SymbolPrimitive = this.last,
children: List<Person> = this.children,
metas: MetaContainer = this.metas
) {
/* ... */
}
/**
* Determines if this [Person] is equivalent [other].
*
* To be equivalent, [other] must be an instance of [Person] and all of its properties (except [metas]) must be
* equivalent.
*
* This is recursive and applies deeply to all child nodes.
*
*/
override fun equals(other: Any?): Boolean {
/* ... */
}
/**
* Computes a hash code for the current instance of [Person].
*
* The has code is computed once, the first time this function is invoked and the value is then re-used.
*
* The [metas] property is not an input into the hash code computation.
*/
override fun hashCode(): Int {
/* ... */
}
}
// ... other stuff
}
The first thing the reader will note is that Person
resides within the SampleTypeDomain
class which is being used as
a namespace. Some projects have many type domains sharing the same class names and this helps avoid ambiguity and
namespace pollution.
The next thing the reader might notice is that Person
implements some functionality provided by Kotlin
data classes. Such as .copy
, .equals
and .hashCode
.
The data class
feature of Kotlin can't be used in this case because data classes always include all of their
properties in the .hashCode()
and .equals()
implementations, and we do not include [metas] in the definition of
equivalence for any PIG-generated class. (More on metas later.)
Although the constructor of Person
seen in the previous section was public, this is not the preferred way of creating
instances. The preferred way involves using SampleTypeDomain.Builder
. The following code creates a node representing a
person and two children:
val person = SampleTypeDomain.build {
person(
"Billy", "Smith",
listOf(
person("Jack", "Smith", listOf()),
person("Sue", "Smith", listOf())
)
)
}
This approach has the benefit of being very clear and concise because it is not necessary to fully qualify each type or
to add import
statements for each the generated each types. This especially true for large projects with many
PIG-generated types and type domains.
The relevant parts of SampleTypeDomain
which makes this work:
class SampleTypeDomain private constructor() {
companion object {
fun <T : SampleTypeDomainNode> build(block: Builder.() -> T) {
// ...
}
// ... other stuff
}
interface Builder {
fun person(
first: String,
last: String,
children: List<Person> = emptyList(),
metas: MetaContainer = emptyMetaContainer()
): SampleTypeDomain.Person =
SampleTypeDomain.Person(
first = first.asPrimitive(),
last = last.asPrimitive(),
children = children,
metas = newMetaContainer() + metas
)
// overloads of [person] omitted for brevity.
}
// ... other stuff
}
class CalculatorAst private constructor() {
// ... other stuff
sealed class Expr(override val metas: MetaContainer = emptyMetaContainer()) : CalculatorAstNode() {
class Lit(
val value: LongPrimitive,
override val metas: MetaContainer = emptyMetaContainer()
) : Expr() {
// ... API same as "person" type from a previous example
}
class Binary(
val op: Operator,
val left: Expr,
val right: Expr,
override val metas: MetaContainer = emptyMetaContainer()
) : Expr() {
// ... API same as "person" type from a previous example
}
}
// ... other stuff
}
The builder API shown in previous examples works here as well:
// 1 + 2 * 3
val expr = CalculatorAst.build {
binary(
plus(),
lit(1),
binary(
times(),
lit(2),
lit(3)
)
)
}
PIG generates a facility that allows an instance of a Sum Type to be converted to any other type.
Two examples are provided below.
val expr = CalculatorAst.build {
// 1 + 2 * 3 (from the example above)
}
/** Evaluates an instance of CalculatorAst.Expr */
class CalculatorAstEvaluator : CalculatorAst.Expr.Converter<Long> {
override fun convertLit(node: CalculatorAst.Expr.Lit): Long = node.value.value
override fun convertBinary(node: CalculatorAst.Expr.Binary): Long {
val leftValue = convert(node.left)
val rightValue = cvonvert(node.right)
when (node.op) {
is CalculatorAst.Operator.Plus -> leftValue + rightValue
is CalculatorAst.Operator.Minus -> leftValue - rightValue
is CalculatorAst.Operator.Times -> leftValue * rightValue
is CalculatorAst.Operator.Divide -> leftValue / rightValue
is CalculatorAst.Operator.Modulo -> leftValue % rightValue
}
}
}
val evaluator = CalculatorAstEvaluator()
println(evaluator.convert(expr))
// prints: 7
/** Converts an instance of CalculatorAst.Expr into source code. */
class ExprStringifier : CalculatorAst.Expr.Converter<String> {
override fun convertLit(node: CalculatorAst.Expr.Lit): String = node.value.toString()
override fun convertBinary(node: CalculatorAst.Expr.Binary): String {
val leftString = convert(node.left)
val rightString = cvonvert(node.right)
return when (node.op) {
is CalculatorAst.Operator.Plus -> "$leftString + $rightString"
is CalculatorAst.Operator.Minus -> "$leftString - $rightString"
is CalculatorAst.Operator.Times -> "$leftString * $rightString"
is CalculatorAst.Operator.Divide -> "$leftString / $rightString"
is CalculatorAst.Operator.Modulo -> "$leftString % $rightString"
}
}
}
val stringifier = ExprStringifier()
println(stringifier.convert(expr))
// prints: 1 + 2 * 3