Frank Scheffler
Jan 9 · 8 min read
Photo by rawpixel on Unsplash

In my last post I introduced you to some of the more general features of the Kotlin language. This time, I will walk you through creating your own Domain Specific Language (DSL), which is very useful, if you want to build abstractions for your own (or even other) domains. Writing your own DSL definitely doesn’t account for an everyday task, but it’s always worth knowing the advanced capabilities of the programming language at hand.

Motivation

“GeoJSON is a format for encoding a variety of geographic data structures. GeoJSON supports the following geometry types: Point, LineString, Polygon, MultiPoint, MultiLineString, and MultiPolygon. Geometric objects with additional properties are Feature objects. Sets of features are contained by FeatureCollection objects.” [Source: http://geojson.org/]

That said, GeoJSON is often used in applications dealing with geographical data, for instance routing information used for navigation. It can be easily visualized, e.g. as follows:

GeoJSON sample with Berlin, Frankfurt, Stuttgart airports as LineString, city centers as Points, and meta information as properties

The GeoJSON data structure usually consists of a FeatureCollection containing LineString features for representing segments of the route and Point features for representing points of interest, e.g. as follows:

{
"type" : "FeatureCollection",
"features" : [ {
"type" : "Feature",
"properties" : { },
"geometry" : {
"type" : "Point",
"coordinates" : [ 13.404148, 52.513806 ]
}
}, {
"type" : "Feature",
"properties" : { },
"geometry" : {
"type" : "Point",
"coordinates" : [ 8.668799, 50.109993 ]
}
}, {
"type" : "Feature",
"properties" : {
"name" : "Stuttgart"
},
"geometry" : {
"type" : "Point",
"coordinates" : [ 9.179614, 48.77645 ]
}
}, {
"type" : "Feature",
"properties" : {
"description" : "Berlin -> Frankfurt -> Stuttgart",
"distance" : 565
},
"geometry" : {
"type" : "LineString",
"coordinates" : [
[ 13.292653, 52.554265 ],
[ 8.562066, 50.037919 ],
[ 9.205651, 48.687849 ]
]
}
} ]
}

Fortunately, the raw JSON structure does not need to be created manually, as there are libraries such as GeoJson POJOs for Jackson, which allow for GeoJSON creation of the above shown example, as follows:

Creation of GeoJson data structure using Kotlin and “GeoJson POJOs for Jackson”

In Kotlin the GeoJson POJOs can be constructed using their default constructors together with the apply() method, which allows us to refer to the POJO using the implicit this reference. Hence, code within the lambda function passed to apply() automatically refers to the object, it was called on, e.g. geometry referring to the Feature class and add() referring to the LineString. The featureCollection POJO may then be serialized using standard Jackson object mapping.


Although the GeoJSON Jackson library relieves us from creating raw JSON, it is well suited for being wrapped by a Domain Specific Language, a builder style DSL, that further improves the readability and conciseness of the code. The following DSL based code snippet could describe the same information as the one above, in a much more readable manner, though:

DSL-based creation of GeoJson data structure using Kotlin DSL for “GeoJson POJOs for Jackson”

As can be seen, a DSL-based approach, encapsulating object construction, provides some noteworthy advantages:

  • A domain specific language drastically improves the readability of the code, as it’s focused on a specific domain, as opposed to a general purpose programming language, such as Kotlin.
  • However, since the DSL itself is written in Kotlin, type safety is fully preserved. At the same time it provides flexibility, where needed, such as varying order of lat and lng when defining coordinates.
  • An internal DSL isn’t limited to its own grammar, but rather may be extended using regular Kotlin code, as opposed to an external DSL, which is usually based on its own (closed) syntax. For instance, a for loop may be used within the lineString to create a whole set of coordinates. At the same time the DSL need not strictly resemble the creation logic of the underlying API. The distance could as well have been calculated directly from the given coordinates automatically.
  • The DSL decouples the code from the underlying API, that is GeoJson POJOs for Jackson in our case. Thus, the (underlying) API may be easily replaced, if necessary, e.g. by creating the raw JSON directly as part of the DSL.
  • The object creation is fully encapsulated, which allows for cross-cutting concerns to be added to the creation process. Validation logic can be executed transparently, ensuring that the GeoJSON structures adhere to the specification, e.g. a LineString having at least two coordinates.

Throughout the remainder of this blog post I will walk you through the concepts and techniques used to create such a GeoJSON builder DSL.

Object creation using “Lambdas with Receiver”

Apparently, the outermost featureCollection resembles an equally named function in Kotlin with a lambda function parameter passed to it for initialization. It is also obvious, that we want to provide scoped nested functionality to this lambda, such as creating points or lineStrings, which makes no sense outside a featureCollection. In order to achieve this, we can define a DSL helper class FeatureCollectionDsl, that is passed to the initialization (lambda) function given, e.g. as follows:

The main() function exemplarily shows, how the DSL can be used, although at this point it is not yet fully functional. The FeatureCollectionDsl also provides a toGeoJson() function for creating the GeoJson POJO, which isn’t complete yet, as we will focus on collecting nested elements for construction later. For now, let’s assume any GeoJson element in our DSL provides such a function for finally creating the POJOs.


The problem with this approach is that the initialization function passed to featureCollection() needs to refer to the scope using the it variable (line 23) or another specifically chosen parameter name (e.g. point), in order to access nested functionality. The same applies to the point() initialization.

Preferably, the nested scope should be directly accessible by the lambda function, instead. Fortunately, the this reference in Java/Kotlin provides this behavior out of the box i.e., it is referred to implicitly, as in most other languages. Kotlin enables us to apply dedicated this references to higher order functions using so-called lambdas with receiver. As the name suggests, such a lambda is defined using a dedicated receiver object type, which may differ from the object type of the original function call. Defining lambda parameters with receivers is comparable to defining regular extension functions for a type, more precisely for a higher order function type. Rewriting our DSL using lambdas with receivers allows us to omit explicit parameter reference in favor of implicit this, e.g. as follows:

The init parameters defined in lines 6 and 16 are both defined as lambdas receiving PointFeatureDsl and FeatureCollectionDsl implictly. This drastically simplifies the point() calls in line 23–24, avoiding the it reference. Furthermore, the initialization function supplied to point() has been provided with an empty default implementation (line 6), which enables us to skip the initialization, as shown in line 23.

Setting properties using Infix Functions

Next we want to be able to apply key-value pairs as properties to GeoJson features, Point and LineString in our case. While we could define a map-like add(key: String, value: Any) function for this, Kotlin allows us to define so-called infix functions, which enable us to avoid the parentheses of the function call altogether, by writing "distance” value 42.

Infix functions are used in Kotlin’s standard library, as well. The to function represents a popular example, allowing us to declare Pairs in a similar way.

The interesting aspect here is, that the infix function value needs to be defined on the String class somehow, since we want our property keys to be of that type. While a global infix extension function could be used for this, we want to restrict the visibility of the function to the nested receivers of our Point and LineString features. Fortunately, we can achieve this in Kotlin by defining a member function as infix extension for the String class e.g., as follows:

The FeatureDsl thereby serves as base class for all GeoJson features that can have properties applied to them. In line 5 it defines the aforementioned infix function that allows us to define properties as key-value pairs and collects them internally in a regular Kotlin (mutable) map. Thus, any class extending from FeatureDsl automatically inherits the scoped infix function, so it can be used during nested object initializing receiver functions.

Note, that this in the scope of the extension function String.value() naturally refers to the String class. In case of naming conflicts when referring to the parent scope of the FeatureDsl class, labels can be used, i.e. this@FeatureDsl.

As mentioned earlier, wrapping the key-value assignment in a DSL, enables us to transparently validate input data i.e., preventing duplicate property assignments, as shown in line 6.

Object initialization using infix functions

A LineString feature consists of properties and a list of coordinates. While we can define a nested coord(lng, lat) function, infix notation allows us to define a more readable DSL here. In the end, the coordinates that make up the line string do not have any nested properties. Accordingly, we could define them right away using coord lat 1.1 lng 1.2 or coord lng 1.2 lat 1.1, respectively.


At first glance it may not seem obvious, how this syntax relates to Kotlin language constructs, so let’s look at the DSL first:

LineStringFeatureDsl defines both a coord() function for defining (and storing) a coordinate using a regular function call as well as a coord computed property. The latter one serves as entry point to the fluent-style coordinate definition and returns aCoordStart instance, which is defined as inner class, so that access to LineStringFeatureDsl.coord() is still possible.

CoordStart then defines both lng() and lat() infix functions returning CoordLng and CoordLat, respectively. This is necessary to provide the flexibility to define latitude and longitude in any preferred order. Both intermediate objects store the given infix parameter (either lat or lng) and provide another infix function for supplying the missing lng or lat value. Finally, each of them calls the regular coord() function to complete the fluent syntax chain and store the coordinate appropriately.


As can be seen, the combination of both computed properties and infix functions enables us to define more human readable DSLs, relieving the end user from regular function call syntax, focussing on the essential DSL definitions instead.

Putting it all together

Now that the syntax of the domain specific language has been defined, there are some final tasks left, in order to produce valid GeoJson POJOs:

  1. The data provided as part of the DSL needs to be collected within the DSL objects until object creation of the surrounding element is finished. We have already seen, how Feature properties have been collected in a regular Kotlin map. The same applies to collecting latitude/longitude coordinates.
  2. The collection can partly be combined with the final construction of the GeoJson POJO objects. For instance, latitude/longitude pairs can be converted directly into LngLatAlt objects. Finally, the Points and LineStrings belonging to a FeatureCollection can be stored in a regular Kotlin list, before the feature collection is finally converted into its GeoJson representation.

The following code shows the complete GeoJson DSL, as dicussed so far:

Kotlin-based GeoJson DSL

Since both Point and LineString represent GeoJson features, they can be collected using their common base class, which we introduced for storing optional feature properties. Accordingly, FeatureCollectionDsl defines a generic add() method (line 67–70), that both initializes the corresponding features with its initialization function, triggers the GeoJson POJO creation, and collects the result.


In this blog post I introduced you to the techniques for building your own domain specific language with Kotlin and the benefits, that custom DSLs bear. The DSL shown here can also be found on GitHub.


Feel free to respond to me or follow me on Twitter.

Digital Frontiers — Das Blog

Dies ist das Blog der Digital Frontiers GmbH & Co. KG (http://www.digitalfrontiers.de). Hier veröffentlichen wir zu Themen, die uns interessieren und bewegen.

Frank Scheffler

Written by

Senior Solution Architect @dxfrontiers

Digital Frontiers — Das Blog

Dies ist das Blog der Digital Frontiers GmbH & Co. KG (http://www.digitalfrontiers.de). Hier veröffentlichen wir zu Themen, die uns interessieren und bewegen.

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade