Feature Request: omit "this" in infix method call, for cleaner DSLs

So basically, allow us to replace the following:

this answer 42

With this:

answer 42

This is particularly expressive when the right-hand operand is more complicated:

answer 12 * 3 - (8 - 14) * 2

There’s also the case where the call isn’t the root of the expression:

val x = capture 3 + 4
bar(release x)

This is especially nice as you can modify an existing statement by dropping in your method name and not worrying about the trailing parenthesis of an equivalent non-infix method call (which doesn’t necessarily improve legibility anyway, like in these cases, imo.)

One counter-argument would be that the following is confusing if implemented consistently:

answer 38 plus 4

I would propose to special case the precedence here and always evaluate the infix call after the right-hand operand is evaluated. Yes, it would be one more thing to learn when learning Kotlin, but it’s a pretty small tradeoff in this case imo, and would allow for more expressive DSLs.

Another problem is that, while I would really enjoy the syntax for the non-root case, it does seem more prone to misuse:

val x = capture 3 + capture 4
val x = capture 3 plus capture 4
// How would these even parse? Could it be done as we might intend?

Parsing aside, any feature can be misused, so I suppose the question is more about the likelihood of it happening, and understandability to newcomers. Perhaps it could only be allowed in the cases where it’s totally unambiguous, such as the right side of the an assignment statement or as a method parameter.

Thoughts? I’m sure I haven’t considered everything here. Perhaps there would be better way of achieving this in the language without touching infix methods? Allowing on methods more generally sounds pretty extreme, whereas a new “prefix” method qualifier might be clearer, while disallowing calls on existing infix methods in an unintended way.

3 Likes

Just a little side note, but if this is implemented you can then have an infix fun like this:

infix fun Any?.myPrefixFun(a:Int):Int = a + 5

class SomeClass {
    fun someFun() {
        println(myPrefixFun 37)
    }
}

Which is basically a subset of prefix funs in disguise

2 Likes

What is the output of:

fun main() {
	Test.run()
}

object Test{
    fun run(){
        log 1
        log 2 log 3
          log 4
        log 5
    }
}

infix fun Any.log(arg:Any){
    println("[$this] $arg")
}

If I remember well, this was a Scala mistake.

1 Like

That is a very good example of how this can go horribly wrong yes, but I feel like maybe having very strict rules for it can work here? Like just having similar rules as how unaryPlus and unaryMinus works might help here. For example, here’s your code but rewritten using plus and unaryPlus

fun main() {
	Test.run()
}

object Test{
    fun run(){
        + MyInt(1)
        + (MyInt(2) + MyInt(3)) // According to the proposal, one of the behaviours of these infix funs would be that their 2nd argument is the whole rest of the line, and so this is emulated here with ()
          + MyInt(4)
        + MyInt(5)
    }
    
operator fun Any.unaryPlus(){
    println("[${this@Test}] ${this@unaryPlus}")
}
}

operator fun Any.plus(arg:Any){
    println("[$this] $arg")
}

inline class MyInt(val i: Int)

And so, I’m guessing here that, as a rule, it can be implemented so that if there’s anything on the RHS that can work as the infix function receiver, then that is used, but if there isn’t, then one of the receivers in scope is used then

1 Like

Just so I understand, the only value added is that you don’t have to write the parentheses, right?

answer(42)
answer 42

Groovy allows this for all methods–I really dislike it. Limitations might help but I would still expect it to be considered bad practice to make a prefix function outside of dsl usage.

Personally I’m not much of a fan since we gain so little. Are there any other use-cases outside of DSLs?
Seeing it used commonly in Groovy has lead me to find it harder to read–the loss of consistency is a big one.

Prefix functions (and infix functions) have their “fixation” (?) as part of their API. Once you publish a function as such, you make it easier to make a breaking API change. Limits could help here as well but that brings us back to the “is it worth it” counter argument.

I like the approach to language design where the use-case is the main focus instead of a desired solution. That way alternative solutions, or better yet, ways to make the problem irrelevant can be discovered. If the problem here is succulent DSL creation, we shouldn’t limit ourselves to the prefix solution.

4 Likes

Just so I understand, the only value added is that you don’t have to write the parentheses, right?

Basically, if you’re comparing it to a single parameter non-infix method - you could also view it as an infix method where you can omit “this” as a first parameter, though I’m starting to think a separate “prefix” category would be a better conceptualization.

Seeing it used commonly in Groovy has lead me to find it harder to read–the loss of consistency is a big one.

Totally agree, but I also see situations in which it really improves legibility - ideally, those cases could be better captured by establishing limitations (as you mentioned). Some possibilities:

  • Method must be declared explicitly “prefix”. We could also allow the traditional syntax with parenthesis in this case, though that might appear inconsistent and thus harder to understand. This would allow the method to be called outside other established limitations, however. Java-compatibility would demand this on the Java end, anyways. This could also allow backwards compatibility for existing DSLs to “upgrade” (the Kotlin Gradle DSL comes to mind.)
  • Prefix calls could be of a precedence just beneath infix calls (precedence table). Call chaining could be a warning via inspections, or even outright forbidden depending on how much we despise it, lol. You will have people running into a wall after writing their code for this use case, however, and they might obstinately ignore such a warning given the sunk cost of having written their untested library, etc.
  • For the above, you could generalize the warnings/errors for whenever a “prefix” call occurred inside a same-or-higher precedence construct. E.g. foo 3 + foo 4 which actually resolves to foo (3 + (foo 4)). I’m willing to hear a more elegant way of dealing with these cases, however.

I like the approach to language design where the use-case is the main focus instead of a desired solution.

I think DSLs are a legitimate use-case, and the gradle DSL is a good example of that. For example, this:

dependencies {                              
    api("junit:junit:4.13")
    implementation("junit:junit:4.13")
    testImplementation("junit:junit:4.13")
}

Becomes:

dependencies {                              
    api "junit:junit:4.13"
    implementation "junit:junit:4.13"
    testImplementation "junit:junit:4.13"
}

At the risk of being overly specific, the use case I’ve been working on is a high-performance game math library, where math-primitive temporaries are pooled in scoped blocks. What I’d like is the ability to “capture” temporaries, that otherwise get reconsumed by operations they’re passed into as a significant optimization.

// What I currently have to do:
val x = capture(vec3f(1f, 2f, 3f) + 3f)

// What I'd like to do:
val x = capture vec3f(1f, 2f, 3f) + 3f

Syntax highlighting really helps the prefix case as well. Of course, this all goes out the window whenever the JVM gets value types, but still! The ugliness of the current capture syntax made me scrap the “reconsuming temporaries” paradigm and just allocate from the pool every operation, but without it I go from about a 10% to a 20% performance loss over managing temporaries by hand… ouch.

That way alternative solutions, or better yet, ways to make the problem irrelevant can be discovered. If the problem here is succulent DSL creation, we shouldn’t limit ourselves to the prefix solution.

Hmm, maybe a colon after the method’s name might be more legible, if not more Groovy-like:

answer: 42

val x = capture: 3 + 4
bar(release: x)

Just thinking out loud :stuck_out_tongue:

1 Like

Yes, I am all for explicitly disallowing or at least warning in this case, lol. IntelliJ has a great inspections tool to lean on in cases like these, and I believe Kotlin and IntelliJ are sufficiently married to consider this an adequate solution (correct me if this is a bad assumption.) See my proposal in the last comment to warn or fail in all cases where a “prefix” method is invoked inside a same-or-higher precedence construct. Sandwiching a colon into the construct may change things, not sure.

1 Like

I don’t see any great language enhancement in this feature, especially compared to downsides.
Warnings in compiler/IDE can’t really fix a bad language design.

So adding “don’t have to write the parentheses”, subtracting “this can go horribly wrong”, minus 100 points… not so much.

7 Likes

I don’t like it, reminds me on Haskell to reason about when infix gets preferred over prefix.

If ever, I would only allow it in case of a single prefix call without any infix/chaining call in a line, otherwise you need anyway some parentheses to disambiguate.

When I see this code, I already see the value of not allowing it. To me it is not clear whether the + 3f is applied to the result of capture or to the vec3f. For this particular version I might even go with an inline capture version that takes a function. It is all about communicating clearly what happens.

Of course there is still the bits about:

  • How can this go wrong?
  • How does this encourage poor style?
  • How does this affect readability, learnability and predictability?
  • Will doing this significantly limit future language evolution (grammar is exclusive)?
  • The default -100 points
8 Likes

Just throwing in another thought. I know this is not at all a “solution” to what OP wants, nor a new language design proposition, but maybe you like it anyway.

Instead of focusing on the function value (instead of function(value)) syntax, you could embrace the (true) infix style by defining a (probably meaningless) “domain object” on which all infix funs act. So, as you mentioned, you’re working on a game math lib. Idk how you call it, but let’s call it GMath just for now. Then you could do this:

object GMath {
    // might contain code, but does not have to, necessarily
}

// ...

infix fun GMath.answer(expr: Int) {
    // ...
}

infix fun GMath.capture(expr: Int) {
    // ...
}

infix fun GMath.release(expr: Int) {
    // ...
}

// whatever else you need

You would then call it like this:

GMath answer 42
GMath capture 3 + 4
GMath release x

Where GMath is an object which is, in itself, meaningless, but contextually triggers the GMath DSL. This way, it also gets easier to not mix up other top-level DSLs (basically it prevents cluttering the top-level scope).

The approach is similar to what you usually see in NumPy, where every operation is usually prefixed by np., or recently as well to multik (which is of course heavily inspired by NumPy), where the prefix would be mk. You can see how they implemented this pattern here. Multik is the previously mentioned “domain object” and there’s also a typealias (mk) to shorten the prefix.

As said, if your point really is that you want prefix functions in Groovy style, this will not help you. But if you just want to look for a decent-looking way of implementing your DSL, you might consider this.

In any way, I hope this helps you or gives you some kind of helpful inspiration. Don’t forget: In the end, developing Kotlin DSLs is a very creative process, nothing you could ever argue about with technical details!

3 Likes

First comment: would adding a colon change your opinion? I.e. val x = capture: 3 + 4. Not sure how it would pan out grammatically, but it looks significantly better imo.

Second comment: how do you feel about the existing infix syntax? Seems like similar arguments can be made about it, but it still exists in the language. E.g., I can write things that are inscrutable, are subtle bugs, etc, all of which can naturally follow from whatever domain I’m programing in:

val foo = bar foo 3 + 4 baz bar

val cosine = unitVec dot otherVec / len(otherVec) // Works, but has an unintended allocation!

val x = MyLib capture up dot facing + facingOffset // Not legible, and has a bug

val x = capture: up dot facing + facingOffset // Significantly more legible, and would work as intended

I can even make the same argument about plus and minus:

val num = -3+-+-+-+-+-+4 // totally valid Kotlin!

// Or to borrow fvasco's example, imagine unaryPlus and/or plus are overloaded:
fun MyDsl.run(){
    + 1
    + 2 + 3
      + 4
    + 5
}

// Actually plausible use-case if we overload unaryPlus, as many libraries do:
suspend fun GameDialogueDsl.demo() {
    + "Hi!"
    + "I am " + "an NPC"
      + " whose dialogue got split unintentionally!"
    + "Huh?"
}

// What I would rather have:
suspend fun GameDialogueDsl.demo() {
    say: "Hi!"
    say: "I am " + "an NPC"
      + " whose dialogue didn't compile!" // because String.unaryPlus is no longer defined!
    say: "Thanks compiler!"
}

Which is to say, we can always generate hypotheticals, the only question is how likely the programmer is to actually produce them. So while I agree the non-colon case is harder to read when combined with infix operators (a likely occurrence), I have yet to see an argument against the colon case when compared to existing language features. The fact that so many DSLs overload unaryPlus/Minus is actually a big reason to include this feature imo, i.e. to deprecate those methods.

EDIT: I regret that my proposal has changed since the original post, but that’s the beauty of discussion I suppose. With the colon, I think the need for warnings and “disallowed cases” disappear, since it’s as least as understandable as the existing math operators in context.

4 Likes

I agree that as stated there could be issues, but wonder if there might be a way with some additional prefix character to be explicit to say treat this as infix on the receiver. What I am imagining is something like:

.answer 42

but this could get ambiguous so something besides dot. There are languages that use a … prefix for similar things.

or maybe it is a suffix character:

answer: 42

But it is a hard sell to just save 1 character

I would also point out that it may be more complicated than just these two alternatives

answer(42)
this answer 42

It may be that you have to qualify this:

this@someReallyObnoxiousOuterScopeName answer 42
1 Like

Dart has a … operator that is kind of like a simpler version of apply method that would be nice here. It doesn’t have infix functions but in a case like you laid out it can do

GMath
   ..answer(42)
   ..capture(3 + 4)
   ..release(x)

Which in Kotlin would be something like:

GMath
   .apply { answer(42) }
   .apply { capture(3 + 4) }
   .apply { release(x) }
1 Like

Well, this would again just be regular functions, no infix, no prefix. Just that in Kotlin you’d most likely use a scoped function like with or run for that matter. The other alternative would be to make answer, capture and release top-level, but you’d run in the aforementioned problem of top-level scope pollution. Scoped functions actually still adress this problem, but give you back the conciseness of top-level symbols:

// with top-level definitions:
answer(42)
capture(3 + 4)
release(x)
// concise, but scope pollution
// with member / extension definitions
GMath.answer(42)
GMath.capture(3 + 4)
GMath.release(x)
// not as concise, but no scope pollution
// member / extension definitions + scope functions
with(GMath) {
    answer(42)
    capture(3 + 4)
    release(x)
}
// no scope pollution, but more concise that the previous version

While scope functions are a beautiful solution to this problem, it is actually barely related to what OPs problem, or suggestion, is. In fact, changing the definitions to infix, we get back to the original problem in the third example, where we have to use this as the left operand.

I support the idea of “parentheses-less prefix function call” when it does not have a value and acts as a custom statement like return/throw, not as an expression.

prefix fun DslDomain.answer(value:Int) { ... } // Return type must be Unit

answer 42
answer 12 * 3 - (8 - 14) * 2

generateSequence {
  yield value
  yield anotherValue
}

buildString {
  append "Foo("
  append value
  append ")"
}

It would be a safer alternative to the already existing option of overriding unary operators.

div {
 t "Hello "
 raw "<b>World</b>"
}
// vs
div {
 +"Hello "
 !"<b>World</b>"
}
5 Likes