Programming DSLs in Kotlin by Venkat Subramaniam

Programming DSLs in Kotlin by Venkat Subramaniam

Author:Venkat Subramaniam
Language: eng
Format: epub
Tags: Pragmatic Bookshelf
Publisher: Pragmatic Bookshelf


Use Multiple Contexts to Remove Ambiguity

We may want to create and use multiple context objects for two different reasons. By creating multiple contexts, we can gain these advantages:

Simplify code, reduce code bloat, make code more cohesive, honor the single responsibility principle, and make code easier to maintain

Reduce the chances of invalid or out-of-sequence function calls

We discussed the first benefit in the previous sections. In this section we’ll focus on the second benefit. When a single context is used, it may be hard to keep the user from making an invalid sequence of calls. Let’s take a look at an example that exposes this issue and find a solution using multiple contexts.

Suppose we’re designing a register and want to implement an add facility. To introduce fluency, we may design an add() and an and() function, like so:

context/ambiguous.kts

​ ​object​ register {

​ ​var​ total = 0

​

​ ​infix​ ​fun​ ​add​(op1: Int): register {

​ total += op1

​ ​return​ register

​ }

​

​ ​infix​ ​fun​ ​and​(op2: Int) {

​ println(​"adding $op2 to $total"​)

​ total += op2

​ }

​ }

The benefit of this design is the functions may be used fluently, like so:

context/ambiguous.kts

​ register add 4 and 3

Unfortunately, that design has a negative consequence. The design may not prevent a user from inadvertently making a few calls that may not make sense. For example, a user may make these unintended calls:

context/ambiguous.kts

​ register and 3 add 4 ​//ERROR​

​ register and 3 and 4 ​//ERROR​

​ register add 3 add 4 ​//oops​

A good design should enable ease of use and at the same time prevent users from inadvertently falling into traps. Ideally, an unintended or non-sensible use should result in an error instead of quietly misbehaving. Let’s examine how our design fared for catching these kinds of errors.

It turns out that the first call, invocation of and followed by add, thankfully results in a compilation error. The reason for this error is the result of and() being a Unit and having no add() function on it. Yay.

The second call also turned out to be an error—kudos—because and() returns Unit, which doesn’t have an and() function.

The last call, unfortunately, will execute without any error and produce an unexpected behavior. We should design in a way that such calls fail fast at compilation time. Let’s rework the design to achieve this goal.

In our modified design, we’ll introduce two separate contexts, one for the add() function and the other for the and() function.

context/register.kts

​ ​object​ register {

​ ​infix​ ​fun​ ​add​(op1: Int): AndOp {

​ ​return​ AndOp(op1)

​ }

​

​ ​class​ AndOp(​val​ total: Int) {

​ ​infix​ ​fun​ ​and​(op2: Int) {

​ println(​"adding $op2 to $total"​)

​ }

​ }

​ }

Since only the add() function is present on register, we can’t call the function and() directly on it anymore. Also, since add() returns AndOp, which doesn’t have an add() function, we can’t make another call to add() on the result of a call to add(). The following syntax is still valid, as we’d like it to be:

context/register.kts

​ register add 4 and 3

However, all three of the following will fail quickly—illustrating how splitting the context helps to deal with errors more reliably.



Download



Copyright Disclaimer:
This site does not store any files on its server. We only index and link to content provided by other sites. Please contact the content providers to delete copyright contents if any and email us, we'll remove relevant links or contents immediately.