Allow any literal outside parentheses if last parameter?


#1

Why is the last parameter to a function only allowed outside of the parentheses if it’s a lambada? I understand that this is where the biggest gain is, but allowing any type - or at least literals - to go outside the argument list if it’s the last parameter would also improve the readability of DSLs. For example, converting a Groovy Gradle script to Kotlin requires adding a lot of parentheses that are optional in Groovy, and conventionally omitted to improve readability.


#2

Nobody for or against?


#3

You did not present any actual examples, so nobody will seriously consider the proposal.


#4

Borrowed from https://github.com/ktorio/ktor-samples/blob/master/app/youkube/build.gradle

dependencies {
    compile "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
    compile "io.ktor:ktor-server-netty:$ktor_version"
    compile "io.ktor:ktor-auth:$ktor_version"
    compile "io.ktor:ktor-locations:$ktor_version"
    compile "io.ktor:ktor-html-builder:$ktor_version"
    compile "ch.qos.logback:logback-classic:$logback_version"
    compile "org.ehcache:ehcache:3.0.0.m4"
    compile "com.google.code.gson:gson:2.7"
    testCompile "io.ktor:ktor-server-test-host:$ktor_version"
    testCompile "io.mockk:mockk:$mockk_version"
}

instead of

dependencies {
    compile("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version")
    compile("io.ktor:ktor-server-netty:$ktor_version")
    compile("io.ktor:ktor-auth:$ktor_version")
    compile("io.ktor:ktor-locations:$ktor_version")
    compile("io.ktor:ktor-html-builder:$ktor_version")
    compile("ch.qos.logback:logback-classic:$logback_version")
    compile("org.ehcache:ehcache:3.0.0.m4")
    compile("com.google.code.gson:gson:2.7")
    testCompile("io.ktor:ktor-server-test-host:$ktor_version")
    testCompile("io.mockk:mockk:$mockk_version")
}

Not a major difference (although it looks more significant in the context of a complete build file), but the restriction to lambadas seems to be needlessly making a special case out of something that would be more useful in the general case.


#5

The above is already possible in DSLs with an infix function declared on the lambda’s receiver. However currently you need to write this foo "bar". Maybe it could be considered to allow omitting “this” in such case.


#6

I humbly propose that this ought to be fixed at the language level - making lambadas a special case brings no benefits and imposes an unnecessary restriction.


#7

The general solution might be complex to parse. If it means a huge factor in compilation time, I would take my proposal over the general one.

Also my proposal was in reply to the only example you have given, which can be done with my proposal also.


#8

Limiting this to literals shouldn’t have a significant impact on parsing, and is still much more useful than just lambadas.


#9

I don’t think the restriction on lambdas is an unnecessary restriction. I like the idea of a more flexible syntax for DSLs but I don’t think it’s a good idea to allow this in general. I like @fatjoe79’s idea of allowing for an implicit this when using infix function or maybe even a new keyword for those kind of functions:

dsl fun Foo.foo(arg: String) {}

IMO this should however be restricted to functions with a single argument and only work with functions that are declared that way. I for one would not like to see something like pythons (2.x) print statement print 42 instead of print(42). While I agree that a “dsl” syntax without parentheses would be amazing for DSLs like kotlin gradle, it would make the language much harder to read when used in a normal programming environment.


One argument against reusing the infix keyword is that infix as a term is associated with a binary operation. Therefor I don’t think we should use it here, because we are talking about unary operations, which happens within a context. This would also mean that we maybe should prohibit the following

dsl fun Foo.foo(arg: String) {}
val f = Foo()
f foo "test"   // this is bad?! f is the context of and not a part of the operation
with(f){
    foo "test" // this is ok
}

#10

Why is f "string" so much harder to read than f { doSomething } ?


#11

Code already tends to become complex. This will only become worse if there are multiple ways to do something as basic as calling a function. As long as it’s a single line of code it does not matter, but it becomes problematic in more complex situations

fun foo(arg: String) ...
val foo: String = ...
fun bar(arg1: String, arg2: String = "") ...

bar(foo "foo")

This would be a correct way to call bar. The question is, did you just forget to add a comma or not? It’s a very hard to spot bug.


#12

The proposition is quite close to Groovy syntax and kotlin developers rejected the idea at the start. I think that it was a good decision. I like Groovy very much, and it is really very good for DSLs, but not so good for statically typed language. Luckily, it is quite easy to use Groovy DSL parser on top of Kotlin, so you really can write a DSL in groovy and all important parts in kotlin.


#13

@Wasabi375 how is your example different from this valid code?

fun foo(arg: () -> String) = arg()
val foo: () -> String = { "foo" }
fun bar(arg1: String, arg2: () -> String = { "" }) = arg1 + arg2()

bar(foo {"foo"})

#14

bar(foo()) { ... } or even bar(foo) { ... } is much harder to confuse with bar(foo { ... })


#15

The argument against this and the implicit this is that it very quickly leads to ambiguous code that can only be interpreted based on the definitions that are in scope. You already have that a little with infix operators but that is very limited.

Consider a line like:

foo bar baz

If these two proposals were allowed there are many meanings for that code.


#16

Thanks for the insight @dalewking. First I thought that it should be fixable with precedence like for the expression foo + bar * baz. But it seems to be a bigger problem. There arr potentially 2 ways to interprete this example with operators, but there are quite some more ways to interprete your example. The tokenizer cannot even be sure whether bar is an infix function or a regular function taking 1 argument.


#17

@dalewking that was why I suggested (in my original proposal - I’m not sure if you were referring to that) that this could be limited to literals, making it no harder to parse than the current state of limiting it to lambadas.