How to create Kotlin DSL - DSL syntax Kotlin
Asked Answered
M

2

9

As with anko you can write callback functions like this:

alert {
    title = ""
    message = ""
    yesButton {
       toast("Yes") 
    }
    noButton { 
       toast("No")
    }
}

How can I create a nested functions like that? I tried creating it like below but doesn't seem to be working.

class Test {
    fun f1(function: () -> Unit) {}
    fun f2(function: () -> Unit) {}
}

Now, if I use this with extension function,

fun Context.temp(function: Test.() -> Unit) {
    function.onSuccess() // doesn't work
}

Calling this from Activity:

temp {
    onSuccess {
        toast("Hello")
    }
}

Doesn't work. I am still lacking some basic concepts here. Can anyone guide here?

Motorway answered 8/9, 2017 at 10:0 Comment(1)
Take a look at this: kotlinlang.org/docs/reference/type-safe-builders.htmlRufinaruford
K
15

Kotlin DSLs

Kotlin is great for writing your own Domain Specific Languages, also called type-safe builders. As you mentioned, the Anko library is an example making use of DSLs. The most important language feature you need to understand here is called "Function Literals with Receiver", which you made use of already: Test.() -> Unit

Function Literals with Receiver - Basics

Kotlin supports the concept of “function literals with receivers”. This enables calling visible methods on the receiver of the function literal in its body without any specific qualifiers. This is very similar to extension functions, in which it’s also possible to access members of the receiver object inside the extension.

A simple example, also one of the coolest functions in the Kotlin standard library, isapply:

public inline fun <T> T.apply(block: T.() -> Unit): T { block(); return this }

As you can see, such a function literal with receiver is taken as an argument block here. This block is simply executed and the receiver (which is an instance of T) is returned. In action this looks as follows:

val text: String = StringBuilder("Hello ").apply {
            append("Kotliner")
            append("! ")
            append("How are you doing?")
        }.toString()

A StringBuilder is used as the receiver and apply is invoked on it. The block, passed as an argument in {}(lambda expression), does not need to use additional qualifiers and simply calls append, a visible method of StringBuilder multiple times.

Function Literals with Receiver - in DSL

If you look at this example, taken from the documentation, you see this in action:

class HTML {
    fun body() { ... }
}

fun html(init: HTML.() -> Unit): HTML {
    val html = HTML()  // create the receiver object
    html.init()        // pass the receiver object to the lambda
    return html
}


html {       // lambda with receiver begins here
    body()   // calling a method on the receiver object
}

The html() function expects such a function literal with receiver with HTML as the receiver. In the function body you can see how it is used: an instance of HTML is created and the init is called on it.

Benefit

The caller of such an higher-order function expecting a function literal with receiver (like html()) you can use any visible HTML function and property without additional qualifiers (like this e.g.), as you can see in the call:

html {       // lambda with receiver begins here
    body()   // calling a method on the receiver object
}

Your Example

I created a simple example of what you wanted to have:

class Context {
    fun onSuccess(function: OnSuccessAction.() -> Unit) {
        OnSuccessAction().function();
    }

    class OnSuccessAction {
        fun toast(s: String) {
            println("I'm successful <3: $s")
        }
    }
}

fun temp(function: Context.() -> Unit) {
    Context().function()
}

fun main(args: Array<String>) {
    temp {
        onSuccess {
            toast("Hello")
        }
    }
}
Katharinakatharine answered 8/9, 2017 at 10:52 Comment(12)
Need some time to understand this. Not getting straight into my head :/Motorway
What exactly is hard to get for you, let me provide more info then?Katharinakatharine
Thanks for the response. I'll have a look in the night and then let you knowMotorway
Ok cool. I've extended the description a little bit. The core concept to understand is "function literals with receiver", aka "lambda with receiver"Katharinakatharine
Thanks :) I will have a lookMotorway
I understand a little bit. Let's say, temp is the context function. Now you are calling onSuccess method of temp (Context). Until this is fine. Now going inside it confuses me totally.. :/Motorway
Let's say I am trying to create a syntax like this for an interface with 2 callbacks. Like retrofit callback, textwatcher etc..Motorway
Right, first you call onSuccess (Method of Context) then you call toast (Method of OnSuccessAction)Katharinakatharine
Got it! Thanks! :)Motorway
That's great. I know, it requires another way of thinking and isn't that easy actually. Good luck anyways :)Katharinakatharine
Once you get there, it seems easy. But it was first time, so took a little time for me. Also created something like this with listeners..Motorway
That's great and totally fine, congratsKatharinakatharine
K
1

In your example alert is the function returning some class, for example Alert. Also this function takes as parameter function literal with receiver

In your example you should make your onSuccess the member method of your Test class, and your temp function should return instance of Test class without invoking it. But to have toast to be invoked as in your desire, it has to be member function of whatever class is returned by onSuccess

I think you don't understand exactly how functional literals with receiver work. When you have fun(something : A.() -> Unit) it means that this "something" is the member function of A class.

So

You can look at my blog post : How to make small DSL for AsyncTask

Kelda answered 8/9, 2017 at 12:16 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.