Skip to content

Code Generation

R. C. Howell edited this page Jun 30, 2022 · 2 revisions

Overview

TODO

Type Model

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.)

Builders

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
}

Sum Type

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)
        )
    )
}   

Converter — Convert<T>

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
Clone this wiki locally