From Extension Lambdas to expressive Kotlin DSLs

Extension functions, and Lambdas and even Extension Lambdas have both been discussed already (and are assumed knowledge for this page).  But the uses of lambdas goes beyond just functionality, and into the realm of expressive code.  Expressive code is about having the code represent the ideas behind the code more clearly, allowing better reading of the code and even more importantly, further exploring of the ideas behind to code as a basis for enhancing the technology delivered by the code.

  • Extension Functions/ combination lambda use cases:
    • revision and delivering functionality
      • enhance existing classes (revision)
      • extensions to classes for use within a specific scope
      • dynamic dependency injection of class functionality
    • introducing expressive code
      • introduction example
      • expressive code with a purpose: Building a DSL
  • A DSL with Strings or other literals as keywords
  • Multiple Closures
  • Conclusion

Extension Functions: Use cases

Why not simply have the ‘extension’ function in the class from the outset? Each of the cases below is an example of why functionality should be added as an extension function.

Functionality Driven Use Cases

Enhance existing classes.

If a class is defined outside the current application, for example in a library, then  adding a method to the original class definition is not an option.  Defining a subclass is a suboptimal solution, as functions library functions will still return objects the original class, that lack the extension. Adding an extension method to the original class is a far more seamless solution.   Extension methods of this nature will normally be written in the global scope of the application in order to provide the enhanced class throughout the code. There are simple examples of this use case on the page introducing lambdas.

extensions to classes for use within a another class

An alternative to extending a class at the global scope is to define the extension within a class.  Defining an extension method within a class is different in two significant ways:

  • the extension can be more disruptive of normal use of the base original class, as the new use will be available only in a more tightly constrained amount of code
  • the extension function both contexts, the class being extended as well as the class where the extension is defined

Code within the extension method in this case has access to two different ‘this’ contexts, can bridge functionality between the classes, and has a richer set of attributes to utilise

dynamic dependency injection of class functionality

All lambdas provide for the injection of functionality (as with the lambda parameter to the ‘sortedBy’ function which provides code to isolated the data for sorting), but the next step is when the lambda can inject class functionality, and again a simple of accessing class example was provided in the introduction page.

Introducing Expressive Code through Lambdas

Introduction example

Here is an example of a simple data class for a person.


class Person(var name: String,
    val children:MutableMap=mutableMapOf()
// note the String,Person type modifier may not show in browser
// due to angle brackets - but is needed for all 'children' declarations
)

// to use the class
val peter = Person(name="Peter")

Now an attempt for more expressive code.


class Person (func: Person.()->Unit={}){
    var name = "NoName"
    val children = mutableMapOf()
    init { func() }
}

/// to use the class
val peter = Person{
    name="Peter"
}

At this point, the code is not particularly more expressive. There is extra code in the class declaration, and the only payback is that of converting the round brackets into braces.  Hold the “why” for now, and consider the steps so far.

The data previously define in the constructor, is now within the class.  This means these values are not accepted in the conventional manner in the class constructor, which  now accepts a lambda.  The lambda is an extension method for the Person class, and the lambda accepted now runs in the init method of the class.  This means code to initialise a Person object is now supplied as a lambda, not by conventional parameters.

So the code in the lambda can use any method of the object to calculate initial values, and even use the value of one ‘parameter’ (or value to be set during the init) to calculate another.  The next step is have a practical benefit from this change.

expressive code with a purpose: Building a DSL

Now consider an example when the Person has children.


class Person(var name: String,
            val children:MutableMap=mutableMapOf()
)

// to instance a person with a child
val peter = Person("Peter",
            children=mutableMapOf("Mary" to Person("Mary"))
)

Now imagine if peter had two children, and they had children.  As the data set becomes more complex, expressing that data starts to get less expressive.  It is not just the boilerplate of the ‘mutableMapOf’, but visualising the data.  It may be easier to instance ‘Peter’ and then add the children one by one, and then add children to those children one by one.

Now to try an expressive way to add children.


class Person(func: Person.()->Unit={}){
    var name = "NoName"
    val children = mutableMapOf()
    init{ func() }
    fun child( func:Person.()->Unit):Person{
        val mychild=Person()
        mychild.func()
        children[mychild.name] = mychild
    return this
   }
}
// now to instance Peter with two children, and a grandchild
val peter = Person {
    name="Peter"
    child {
        name="Mary"
        child { name = "Susan" }
    }
    child {
        name="Paul"
    }
}

The end result is that the complexity of the Person class has increased significantly, but creating data structures based on the class has become far more expressive.

In fact, we now have a DSL for defining complex single inheritance trees. Probably more suited to defining elements of GUI which only have one parent rather than people, but hopefully how it works is clear.

If there is only the one case of representing People, then unless that was a complex case, it is hard to justify the extra code of the class definition.  However if the Person class is part of an often used library, and there is substantial data to express, this solution could be a significant step forward.

A DSL with Strings or other literals as keywords.

The DSL defined above was simple, with two ‘keywords’:  ‘Person’ and ‘child’, and one context: ‘Person’.  Each keyword could potentially introduce its own context, and that context can then in turn have its own set of keywords.  An example of this is the kotlinx.html DSL which has a large set of keywords, often with their own contexts.  For example,  the keywords inside a ul block (or lambda) can be different from those inside an image block.  However the techniques covered already are sufficient to build a lambda such as kotlinx.html.

One additional technique is to have literal values act as ‘keywords’, in addition to the way defined functions (with lambda parameters) can act as keywords.

Any type, Strings, Ints, Enums, or other classes can be become keywords for a lambda by using creating an extension invoke function for that type within the relevant scope.  For example consider if our ‘children’ map was enhanced to hold ‘Relative’ data where a relative has a relationship as well as a Person, we could enhance the above example to work with the strings “son” and “daughter”.  This example is to demonstrate techniques, rather than to present an ideal solution modeling the relationships between people:

enum class Relate{
    son, daughter, unknown
}
class Relative(val relate:Relate, val person:Person)

class Person(func: Person.()->Unit={}){

    var name = "Anonymous"
    val children = mutableMapOf()
    init{
        func()
    }
    fun child(relate:Relate=Relate.unknown, func:Person.()->Unit):Person{
        val mychild=Person()
        mychild.func()
        children[mychild.name] = Relative(relate,mychild)
        return this
    }
    operator fun String.invoke(lam: Person.()->Unit) {
        when (this) {
            "boy" -> child(Relate.son, lam)
            "girl" -> child(Relate.son, lam)
            else -> null
        }
    }
// now to instance Peter with two children, and a grandchild
val peter = Person {
    name="Peter"
    "girl" {
        name="Mary"
        "girl" { name = "Susan" }
    }
    "boy" {
        name="Paul"
    }
}

Multiple Closures

Each lambda block creates a new closure, even if the code opening the lambda block already represents a closure or a class. Effectively, multiple contexts are active at the same time, requiring the this@ for resolution of “this” and resolving name overlaps.

Conclusion

The Kotlin DSL structure is powerful, has strong use cases, but should be used with caution to ensure the use is clear. Generally, the goal is code readability.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s