Domain Specific Language in Kotlin: Difference between revisions
Line 276: | Line 276: | ||
builder.tower("sw") | builder.tower("sw") | ||
|builder.apply { | |builder.apply { | ||
keep("keep") | |||
tower("sw") | |||
} | } | ||
|- | |||
|Vararg | |||
|connect("keep"), "sw") | |||
connect("keep"), "nw") | |||
connect("keep"), "ne") | |||
connect("keep"), "se") | |||
|builder.connectToAll("keep","sw","nw","ne","se") | |||
|- | |||
|mapOf | |||
|Intuitive syntax | |||
|builder.connect("sw","nw") | |||
|builder.connect("nw","ne") | |||
builder.connect(mapOf("sw" to"nw"), "nw" to"ne") | |||
|} | |} | ||
==DSL Techniques== | ==DSL Techniques== | ||
{| | {| |
Revision as of 05:11, 13 April 2021
Introduction
Approaches to Extending Code
There may three options for extending a feature
- Change the Object Model
This has imperative code and therefore you will be forced to abstract extend, abstract extend. You be able to build quickly but it does not scale.
- External DSL (e.g. JSON)
E.g. Use json to describe the new features. You will need to extend the parser and write the code. Build will be slow but the scalability will be great
- DSL in Kotlin
Because it is just kotlin building will be quicker and it will scale hmmmmmm
Attributes of DSL Code
- Language Nature Code is meaningful and has a fluid nature
- Domain Focus DSL is focus one problem only
- Limited Expressiveness Supports only what it needs to to accomplished its task
Imperative vs Declarative
val castle = Castle()
val towerNE = Tower()
val towerSE = Tower()
val towerNW = Tower()
val towerSW = Tower()
val keep = Keep()
keep.connectTo(towerNE)
keep.connectTo(towerSE)
keep.connectTo(towerNW)
keep.connectTo(towerSW)
DSL Restricts the syntax to allow better IDE support and keep focus
castle {
keep {
to("sw")
to("nw")
to("se")
to("nw")
}
}
val castle = Castle()
val towerNE = Tower()
val towerSE = Tower()
Kotlin Language Features
Lambda with Receiver Invoke
A lambda with a receiver allows you to call methods of an object in the body of a lambda without any qualifiers. It is similar to the typed extension function but this time, for function types. The idea is similar to object initializers in C# but is extended to functions and in a declarative way.
We pass an object (StringBuilder) with an attibute (String) and a function to use with the two.
fun encloseInXMLAttribute(
sb : StringBuilder,
attr : String, action :
(StringBuilder) -> Unit) : String {
sb.append("<$attr>")
action(sb)
sb.append("</$attr>")
return sb.toString()
}
// When a lambda expression is at the end of the parameter list, you can take it out of the parentheses during invocation.
val xml = encloseInXMLAttribute(StringBuilder(), "attr") {
it.append("MyAttribute")
}
print(xml)
Operator Overloading
Simple Overloading
Overloading Operators is supported in Kotlin
Expression | Function Name |
a*b | times |
a/b | div |
a%b | mod |
a+b | plus |
a-b | minus |
Example
data class Point(val x: Int, val y: Int)
operator fun Point.plus(other: Point) = Point(x + other.x, y + other.y)
val p1 = Point(0, 1)
val p2 = Point(1, 2)
println(p1 + p2)
Point(x=1, y=3)
Unary Overloading Example 1 (plus)
We can overload unary operators too. For example
val s = shape {
+Point(0, 0)
+Point(1, 1)
+Point(2, 2)
+Point(3, 4)
}
In Kotlin, that’s perfectly possible with the unaryPlus operator function.
Since a Shape is just a collection of Points, then we can write a class, wrapping a few Points with the ability to add more:
class Shape {
private val points = mutableListOf<Point>()
operator fun Point.unaryPlus() {
points.add(this)
}
}
And note that what gave us the shape {…} syntax was to use a Lambda with Receivers
fun shape(init: Shape.() -> Unit): Shape {
val shape = Shape()
shape.init()
return shape
}
Unary Overloading Example 2 (Inc)
This allow you to defined how ++ works
operator fun Point.inc() = Point(x + 1, y + 1)
var p = Point(4, 2)
println(p++)
println(p)
Point(x=4, y=2)
Point(x=5, y=3)
Property Override
In kotlin we can override properties like methods
open class Employee {
// Use "open" modifier to allow child classes to override this property
open val baseSalary: Double = 30000.0
}
class Programmer : Employee() {
// Use "override" modifier to override the property of base class
override val baseSalary: Double = 50000.0
}
Extension Functions
Existing Extension Methods
There are many extensions already in the language for Kotlin. In this case languages is the receiver as it thing apply acts on An example of apply is shown below.
val languages = mutableListOf<String>()
languages.apply {
add("Java")
add("Kotlin")
add("Groovy")
add("Python")
}.apply {
remove("Python")
}
This could have been written
val languages = mutableListOf<String>()
languages.add("Java");
languages.add("Kotlin");
languages.add("Groovy");
languages.add("Python");
languages.remove("Python");
Writing Your Own Extension
We just need to specify the receiver class followed by a period. Here is an extension to String
fun String.escapeForXml() : String {
return this
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
}
Using Generics with Extensions
We can use generics to write extensions and the compiler will help.
fun <T> T.concatAsString(b: T) : String {
return this.toString() + b.toString()
}
5.concatAsString(10) // compiles
"5".concatAsString("10") // compiles
5.concatAsString("10") // doesn't compile
Infix Notation
With infix we a provide a more readable experience where the periods and bracket of an extension method can be omitted.
infix fun Number.toPowerOf(exponent: Number): Double {
return Math.pow(this.toDouble(), exponent.toDouble())
}
// We can now call this the same as any other infix method
3 toPowerOf 2 // 9
9 toPowerOf 0.5 // 3
A more complicated example
infix fun String.substringMatches(r: Regex) : List<String> {
return r.findAll(this)
.map { it.value }
.toList()
}
val matches = "a bc def" substringMatches ".*? ".toRegex()
Assert.assertEquals(listOf("a ", "bc "), matches)
Implementation Technics
Function Sequencing
This is when you write the sequence of the function calls.
builder.thing("My thing")
builder.subthing("Sub thing 1")
builder.subthing("Sub thing 2")
builder.subthing("Sub thing 3")
builder.subthing("Sub thing 4")
We can use apply to improve the but apply is not domain specific
builder.apply {
thing("My thing")
subthing("Sub thing 1")
subthing("Sub thing 2")
subthing("Sub thing 3")
subthing("Sub thing 4")
}
Function Chaining
This is when an object returns the next object
builder.thing("My thing")
.subthing("Sub thing 1")
.subthing("Sub thing 2")
.subthing("Sub thing 3")
.subthing("Sub thing 4")
Symbol Table
Nested Builder
Context Variables
=Implementing DSL=s
DSL Transformations
Goal | Original | Transformed | |
Apply | Remove References | builder.keep("keep")
builder.tower("sw") |
builder.apply {
keep("keep") tower("sw") } |
Vararg | connect("keep"), "sw")
connect("keep"), "nw") connect("keep"), "ne") connect("keep"), "se") |
builder.connectToAll("keep","sw","nw","ne","se") | |
mapOf | Intuitive syntax | builder.connect("sw","nw") | builder.connect("nw","ne")
builder.connect(mapOf("sw" to"nw"), "nw" to"ne") |
DSL Techniques
Goal | Usage | ||
Symbol Table | Allows DSL to accept string literals, resolve the dependencies later | symbols.lookup("sw") | |
Builder | Encapsulate object construction and hold the DSL functions | builder.keep("keep") | builder.tower("sw") |
Function Chaining | Reduce calls to the builder. Make sentences | build.keep("keep")tower("sw") |