Resolving the application of lambdas as parameters


#1

I came across an interesting case, and wonder why the language should not be able to resolve it.

Consider a class ‘Fred’ which requires no parameters, and contains an invoke method which takes a lambda parameter.
So

Fred() {  /* lambda here should work - but will not compile */ } 
(Fred()) {  /* this does compile and does work */ }
Fred() () {  /* so does this  */ }

The first attempt fails with the error “not match for constructor of Fred” (or words to that effect) because even though there is no for the Fred class than can accept a lambda, the compiler assumes the lambda is for the Fred constructor. Is there a reason why handling a lambda cannot be assumed to apply to the result of instancing Fred, rather than the call to instance Fred, when Fred does not accept a lambda parameter?


#2
fun Fred(block: ()-> Unit){}
Fred { }
Fred({ })

fun Jhon(s: Any, block: ()-> Unit){}
Jhon(s) {}
Jhon(s, { }}

When the lambda is the last argument, you can put it outside parenthesis allowing a much cleaner style and easily setup DSLs. When a lambda is the only explicit parameter you can avoid parenthesis at all.


#3

@lamba92 Very true, but I think @innov8ian is talking about something else.
Here’s a runnable example. Feel free to type edits and try running it.

fun main(args: Array<String>) {
    Fred() { } // Does not compile. Try commenting this line out.
    (Fred()) { }
    Fred()() { }
}

class Fred() {
    operator fun invoke(block: () -> Unit) = Unit
}

Personally, I see this is reasonable because when you make the call Fred(), you’re not invoking the object, or calling the invoke method. I understand why one might expect it to work by implicitly calling the invoke method, after all, you are passing a lambda argument. But really you are calling the constructor. It could be confusing to call both the constructor and the invoke method in cases like this–in some cases it would be ambiguous as well.

In order to explicitly call the invoke operator, you can do the following:

fun main(args: Array<String>) {
//sampleStart
Fred() ({ })
Fred().invoke { } // My personal favorite as it's explicit to what you're doing.
//sampleEnd
}

class Fred() {
    operator fun invoke(block: () -> Unit) = Unit
}

And of course you can always call the constructor and the invoke operator separately:

fun main(args: Array<String>) {
//sampleStart
val fred = Fred()
fred { }
//sampleEnd
}

class Fred() {
    operator fun invoke(block: () -> Unit) = Unit
}

#4

thanks @arocnies, as you say, my question was sufficiently unclear that @lamba92 had understandably misinterpreted the question.

However, you clearly nailed exactly what I was raising.

Agree that the Fred().invoke{ syntax could be the clearest available at this time.

The main use case is when creating a literal object enabling DSL like features within the current context. As the DSL lingusitics of ‘Fred’ are requires within the lambda block supplied to the Fred class, there is not normally a use for storing the Fred object in a var, and this interupts the DSL nature of the syntax.

While I agree the explicit .invoke is the best available, in the spirit of DSLs, given that the syntax of

Fred() { // lambda supplied to invoke method of object instanced by Fred()
}

fits within the syntax rules of kotlin, why adjust the compiler so there is no attempt to provide the lambda as an argument to the constructor of Fred, in the situation where no constructor for Fred accepts lambda parameters?

Is there a problem with allowing this that you can see?


#5

This is actually one of the first things that I tried in my own DSLs. Ironically, the IDE complains with a warning that the second pair of parantheses are unnecessary in Fred()(){}. But when they are removed, it will not compile.

I really would like to use the short syntax iny DSLs.


#6

One possible workaround is this

fun fred(block: () -> Unit) : Fred{
    val f = Fred()
    f(block)
    return f
}

#7

I see. The invoke syntax does indeed interrupt the DSL style.

One issue that comes to mind is a signature collision between the constructor with a lambda param and the invoke method. For example:

class Fred(block: () -> Unit) { // Wanting to call this as: Fred() { ... }
    operator fun invoke(block: () -> Unit) = Unit // This is already called as: Fred() { ... }
}

Maybe this name collision isn’t a show stopper? I wonder if there could be an error on the collision while still compiling if there isn’t a collision.

There are some workarounds for a DSL API. Like @Wasabi375 suggested, you could use functions for the DSL instead of objects. (https://github.com/zsmb13/VillageDSL)

You could also try using a companion object’s invoke method that would allow you to call Fred() {}. Here’s a runnable example:

fun main(args: Array<String>) {
    Fred() {}
}

class Fred() {
    companion object {
        operator fun invoke(block: () -> Unit) = println("Companion object call that looks like a constructor call")
    }
}

This sounds like a bug. Do you know if there’s a youtrack issue for this (I wasn’t able to find one, just skimming though).


#8

The problem with the companion method trick is that you can’t pass the instance you’re calling it on, call you? Say, e.g. you want to use the invoke method to change’s Fred’s state.


#9

That’s true, you can’t call the constructors invoke method on an instance of Fred. You would have to create a second method on the Fred class.

You also can’t reference this for accessing an instance of the Fred class. The reason you may want to use the companion invoke method is for the API. The companion invoke can always construct a new Fred and use that instance.

fun main(args: Array<String>) {
   val fredInstance = Fred() {} // This call looks identical to a normal constructor call.
   fredInstance { } // This calls the second method
}

class Fred() {
   operator fun invoke(block: () -> Unit) = println("A second invoke method")

   companion object {
       operator fun invoke(block: () -> Unit): Fred {
           val f = Fred() // You can't access "this", but you can always construct an instance.
           println("Here's a newly constructed Fred instance: $f")
           return f
       }
   }
}

I would use this option when I wanted my DSL to have the same look as calling Fred’s constructor. Personally, I find I usually prefer creating methods for my DSL’s.


#10

A secondary constructor that takes a lambda works well, with or without parens at object creation.


fun main(args: Array<String>) {
    Fred() { } // Now compiles and works
    Fred { }    //  This also works
    (Fred()) { }   // These are
    Fred()() { }   //  still the same
}

class Fred() {
    operator fun invoke(block: () -> Unit) = Unit
    constructor(block: () -> Unit) : this() {
        println("Secondary constructor")
    }
}

#11

Yes, this is my favoured solution right now, too.