Invoke as DSL

To keep it simple I will use this example :

class Greeter(val greeting: String) {
    operator fun invoke(name: String): String {
        return greeting + name  
    }
}

fun main(args: Array<String>) {
    val greeter = Greeter(greeting = "Welcome to Kotlin DSL")
    val greetingMessage = greeter(name = "Zinou")
}

Is it possible to write invoke operator in a DSLish style (if I can say that :slight_smile: ) to have something like :

val greetingMessage = greeter { name = "Zinou" }

Operators don’t differ that much from regular functions, so yes, we can do this and we do this in exactly the same way as usual, for a non-operator function. It will require 5 times more code than in above example and it is generally discouraged to replace named params with DSLs, but yes, this is possible.

1 Like

Can you explain to me why is this discouraged? :thinking: and also how can we achieve that, I want to explore all the possibilities for using DSL to know the next time when and where should I use it , thanks :pray:

1 Like

I think I read somewhere in official docs that DSLs are not meant to replace constructors with named optional parameters. I can’t find the source of this information right now. DSL is harder to implement, harder to maintain and in many cases it doesn’t provide the same level of compile-time guarantees as a regular function.

Anyway, you said specifically that the above is just a simple example. DSL doesn’t make sense for this example, but in your real case DSL could be justified, so please just ignore my comment.

2 Likes

it would be something like:

class Greeter(val greeting: String) {
    operator fun invoke(block: GreeterParams.() -> Unit): String {
        return greeting + GreeterParams().apply(block).name
    }
}
class GreeterParams(){
    var name = ""
}
1 Like

Thanks, @kyay10 I knew that I must use Lambda with receivers but I couldn’t achieve it, now I understand better, maybe this doesn’t make sense to use it here as @broot said, but I would know how we can achieve such elegant DSLish style :+1:

1 Like

I think this is what @broot wanted me to do, we can achieve this (Elegant :joy: ) result with this :

class Greeter(val greeting: String) {
    operator fun invoke(name: () -> String): String {
        return greeting + name()
    }
}

What do you think? :thinking:

1 Like

That is more idiomatic definitely. For instance, require takes a lazy message argument that is a lambda. Keep in mind that this is almost the same as this:

greeter(run { /*Calculate name*/ })
1 Like

I’m a little bit confused :confused: can you explain more please :pray:

I’m trying to say that while this:

greeter { calculateMessageString() }

is neater than this:

val myMessage = calculateMessageString()
greeter(myMessage)

That with the 2nd version you can do

greeter(run { calculateMessageString() })

And so, the “invoke as DSL” syntax doesn’t give you any extra abilities per se, but I do see how it can look neater.

1 Like

I didn’t plan to be too enigmatic :wink:

No, I meant that if we need to pass a single name parameter, then there is nothing elegant in doing this with DSL. I don’t see how greeter { name = "Zinou" } is better than greeter(name = "Zinou"). But I see how it is worse: it requires more code, more maintenance and we can only check required params at runtime, but we can’t at the compile time.

In Java if we have to instantiate a class with many parameters (10+) and most of them are optional, then the builder pattern is a good alternative to a constructor/function with all these params. In Kotlin we have named params and we have optional params and they are better and cleaner approach than bulders or DSLs. At least this is my opinion :slight_smile:

DSLs are for more complicated cases than just passing some params. If your real case is one of such more complicated cases, just use DSL, sure. @kyay10 example shows how you can do this.

3 Likes

I didn’t say using DSL, in this case, will be useful, I know that DSLs are used for more complicated things, I would just explore the ability to use DSL with invoke operator, maybe if I had a class like :

class Broot {
    fun teaching(){
       
 //do something
    }
    fun explain(){
        //do something
    }
    operator fun invoke(): {
        // do something
    }
}

and with DSLs, I would achieve something like

val broot = Broot()
broot{
teaching()
explain()
}

This will make more sense, isn’t it? But this wasn’t my goal

I agree with you that passing a single parameter is not useful but what if you want to use invoke with 3 or 4 parameters that are all required? I prefer to write something like :

{
    firstName = "Broot"
    lastName = "The Best"
}

more than: (firstName = "Broot”, lastName = "The Best" ), this is not the best as you said but neater even if you don’t agree :grin:

1 Like

Well, this is exactly my point. To do this with DSL you need more code, it is more complicated for the user of this function and you get only a limited functionality of the “normal” approach. While using normal function it is clearly visible, which params are required, which of them are optional and the compiler detects if we passed all required params. With DSL we can pass no params at all and the compiler will be fine with that only to crash the application at runtime.

4 Likes