Make parentheses around function calls optional

Groovy has a neat feature where you can drop the parentheses around function calls, making the code appear much more DSLey, so, instead of

add(SomeObject())

You would have

add SomeObject()

I for one would welcome the addition of this feature to the already wonderful tooling for creating internal DSLs.

Check https://kotlinlang.org/docs/reference/functions.html#infix-notation

1 Like

Iā€™m aware, but what Iā€™m proposing is more general (infix function require an explicit right hand argument, my proposal does not, and my proposal could be generalised to omit any function call parentheses except for argumentless calls, so f(a, b, c) could become f a, b, c.

That would be very interesting in my opinion. Another option would be adding a function modifier like infix (prefix would be a good name) that would allow us to use the function without the parentheses.

I ran into this kind discussion somewhere already.

Basically the problem in complexity of parsing function call without parentheses.

Also in Groovy it doesnā€™t work well and causes many problems when compiler ā€œmisunderstandsā€ the code.

Finally the function call without parentheses still requires commas to separate parameters, which still doesnā€™t look DSLish.

Yeah, certainly the single parameter is the cleaner usage, but still ā€œsetOf 1, 2, 3ā€ looks pretty clesn and DSLish to me.

Sad to hear that Groovy has problems with it (but the groovy feature is even more complex since it allows removing dot access, which Iā€™m not proposing here). Besides that, I naively donā€™t see the problem with parsing: whenever you wncounter a function identifier followed by an expression, instert an open paren in the parse tree and set a boolean flag to true. Whenever the next token is an end of statement, insert a close paren. This disallow nested parenless functions, but thatā€™s sensible in my view.

Here is a simple but confusing example:

fun func1(i: Int): Int { return i * 2 }

func1 10 + 15

How do you want this to be interpreted func1(10) + 15 or func1(10+15)?

What if instead of 15 we have conditional expression? How far ahead compiler should look to figure out if function call is over or not?

Other examples:

val list1 = listOf<Int>(1, 2, 3)

listOf<List<out Any?>> emptyList(), list1[1]
listOf<List<out Any?>> emptyList(), list1.get(1)

These 2 line are equivalent, but second one can use any method applicable to both last parameter and return type of the method.

Of cause the priority of operations can help, but it is complex and confusing as is without adding the function call into them. It also makes code less readable.

5 Likes

Iā€™m also not a fan of this idea. The problem as @cabman pointed out is that this will make anything but the most basic calls a nightmare to read and understand.
We already have a similar problem with infix functions, because there is no way of specifying precedence for them.
I see why this would be great for DSL like constructs but I donā€™t think itā€™s worth adding this just for that alone especially as it degrades the language if used for anything else.

3 Likes

Sometimes I also wish omitting the parens in function calls were possible. But it should not be added as long as it causes ambiguous syntax.

In lights of your comments, Iā€™ve become inclined to enable this behaviour with a keyword/modifier akin to the infix modifier. That way, msking this behaviour available would be responsability of whoever creates the function, not whoever uses it.
As for precedence, please excuse me if Iā€™m being naive, but I see it this way: if the parser sees a token that is a function identifier, and the next relevant token is not an open paren, then all the text until the next coma or statement terminator should be understood as a single expression. Thus
fun 1 + 2 is unambiguously interpreted as fun(1 + 2) and if you wanted other interpretation you would have to use parens.
I fear I donā€™t understand whatā€™s the problem with the list examples.

Examples with List can be interpreted as

listOf<List<out Any?>>(emptyList(), list1[1])
listOf<List<out Any?>>(emptyList(), list1.get(1))

or

listOf<List<out Any?>>(emptyList(), list1)[1]
listOf<List<out Any?>>(emptyList(), list1).get(1)

What do yo mean by ā€œstatement terminatorā€? Of cause other than ; which everybody hate.

Yet another example:

func1(3 + 5)
    , 2.0

Compiler cannot figure out the meaning of expression in parentheses without looking forward.

An statement terminator is either a semicolon or an endline in kotlin (setting aside comments) (not sure if ā€˜terminatorā€™ is used in the kotlin spec but I see the term often in defining programming lsnguages).

As per one of my previoud comments, my idea is that the list examples would be understood as in your second translation.

Thanks for the second example, since it helps me flesh out my idea. As per my previoud reply, that snippet would be not only confusing, but illegal. However, func1 2.0, (3+5) would be legal.

The EOL is not terminates anything. For example:

fun func1(i: Int = 0, r: Double = 0.0) {
       val
        x
        =
        i *
                5
}

is very legal 2line expression.

The example you mentioned in last paragraph is legal inside functionā€™s parentheses. Also as I said code func1 (3+5), 2.0 should also be valid, but compiler should look forward for comma to understand how to interpret expression in parentheses.

I think one way to resolve the ambiguity is to let the user explicitly opt-in for this feature.
For example, we can make it only doable in a closure since itā€™s basically a feature for DSL.
The user has to do something like

fun closureExample(@AllowPrefix c: Foo.() -> Unit) { ... }

Only in this way, the user can write:

closureExample { add ā€œabcā€ }

given that the class Foo has method

prefix fun add(s: String) {}
1 Like

In @sam.zhou case there is not much ambiguity in the first place. However one of the problem is if the actual parameter provided as complex expression. This is the same problem as with infix function.

The more complex problem I donā€™t want to have is when function takes multiple parameters.

Also original request was to make parentheses optional. This is not a case with infix function and suggestion by @sam.zhou sounds like to follow the same pattern: if developer provided modifier prefix then no parentheses allowed, otherwise they are required.

Groovy can do this because end of line is much more of a statement Terminator unless the line is incomplete.

Some additional ambiguities. What if we are nesting calls:

fun1 fun2 a, b

Is that fun1(fun2(a,b)) or fun1(fun2(a),b) or fun1(fun2)(a,b)? All could be valid.

What if I need parentheses in my parameter expression?

function (a + b) * c

I could see this causing bugs

Kotlin developers decided against omitting braces based on Groovy experience. It produces a lot of problems and the only place it could be used is gradle-like dsl. For that one can always use groovy wrapper, especially because it much lighter than kotlin compiler. Using brace-less syntax in actual non-dsl code is a terrible mistake.

2 Likes

If Kotlin had builtins for arrayOf/listOf then I donā€™t think Iā€™d see a big need for this feature. In general I find that infix functions can be used in DSL context to provide a marginally more verbose, but much more readable, DSL syntax:

collect { data from source into dest } ā† infix fns from, into defined in scope of collect DSL closure.

i one up this, inbuilt functions without use cases that would bring a lot of confusion and frusturation should have this, also imo @cabman 's example should be directly interpreted as func1(10)+15 if this were to be implemented, also, it shouldnt be applicable for varargs arguments, only ones that you can directly infix when making a normal function should be applicable

I agree with others that this is probably a bad idea but one thing similar I would like to see is the ability to call infix functions that exist on ā€œthisā€ without having to use the keyword this.

So instead of having to say

this inFixFunction value

You could drop the this. But I could see how it could cause confusion in some cases