Infix functions


#1

I saw the thread about dropped features and I was wondering if there have been any discussions about dropping infix functions. I think they make code ambiguous and harder to read and I don't see that they add very much to the language.

My experience of infix method calls in Scala and method calls without parentheses in Ruby is that you have to read the code much more carefully to figure out where the receiver, method and arguments are. Dots and parenthesis make it immediately obvious. Infix calls also make code look inconsistent. Why should there be one syntax for method calls with a single argument and a different syntax for all other method calls?

The biggest downside I can see to removing them is map literals. It won’t be possible to do this any more:

hashMap("a" to 1, "b" to 2)

But there are alternatives. Real map literals. Or overloaded functions for small numbers of args and builders when you have many args (like Google Guava's ImmutableMap). Or functions taking a variable number of tuples of #(key, value). Only joking ;)

I’m worried because it would be very hard to remove infix functions if they’re in version 1.0. But adding them later wouldn’t cause any problems if people decide they’re a good idea after all.

Chris


#2

We are considering making infix calls available only on functions that are explicitly annotated as infix.


#3

I remember the discussion, it was me that brought up the subject. I was just wondering if you were considering going a step further and getting rid of them completely.

I haven’t seen anything that makes me understand the benefits of infix calls but I can see the costs. What do you think they add to the language that makes the cost worth it? I’m genuinely curious.


#4

I agree with you that this feature could produce less readable code in most cases. But in some others, like your hashMap example, it could improve it.

And I like the ability to use infix calls in my code, wherever I think that it improve my code readability.

I also think that unreadable code could be written with any language, independently from the number
of construct that the language offer. It’s our responsibility as programmer to write good code.


#5

Have you seen many examples of Scala code out there on the internet? It shows that if you give developers the opportunity to write unreadable code they will happily take it.

The ability of a developer to write unreadable code is completely dependent on the constructs in the language. You can write code in Scala that’s much harder to read than anything you could do in Java. Java’s inflexibility and verbosity is an advantage in some cases. It’s simple to look at a piece of code and see what’s happening. That simply isn’t true in Scala.

Infix calls give developers the opportunity to be inconsitent. What if you think that infix calls improve the readabilty of your code but someone else on your team prefers to use the normal syntax for all method calls? The codebase will end up a bit of a mess. For something as simple and common as method calls I think it makes sense to have one way of doing things.

The price of flexibilty in a language is complexity and you can only add so much complexity to a language. I’m not sure that infix function calls are a good way to spend Kotlin’s complexity budget.


#6

The use cases we have in mind are

  • Bitwise operations: 1 shl 3, 2 xor 5. Much better than symbols, but introducing keywords for these cases seems to be a a bad idea.
  • Common little things like "to": adding 3 extra characters (especially parentheses) makes it a non-sokution for the map literal problem.

#7

Are those use cases important enough to justify a language feature with such potential for abuse?

Maybe my experience isn’t representative, but I can’t remember the last time I used a bitwise operator. Aren’t they rare enough that introducing symbols wouldn’t be such a big problem?

An alternative to using “to” for map literals would be real map literals in the language. Or built-in support for a Pair literal which could be used for building maps.


#8

Infix calls are dangerous syntax option and some restrictions is good idea.

p.s. I realy like “a” to 1 :slight_smile:


#9

Oh, I've seen lot of unreadable Scala code, and not only on examples, but also in Scala library...

What I’m saying here is that there are ways to mitigate this problem. If you tell your programmers team that infix calls are not to be used
and one of theme use it, then this is a programmer fault, not a language fault.

If I write in Scala ££() to mean  newInstance(), I think it’s my fault, not Scala one. But sure, if a language does not allow to write ££(),
then no one could write it. But you can understand if I write instead a function creaNuovaIstanza()?

Anyway, I agree that infix calls are not a so important feature to spend Kotlin complexity budget on.
I don’t know how much complexity they add to the language.


#10

I don't think there's much potential for abuse if we require an explicit annotation on the declaration site.


#11

You propose a handful of language features instead of one. I prefer one :)


#12

It's not about the number of features, it's about the number of ways they can be misused ;)

I agree that requiring an annotation at the declaration site will prevent a lot of problems. But which functions should be annotated? “xor” and “to” are obvious. What about “map” and “filter”? Where do you draw the line?

It also leaves the door open for people who want to create DSLs that look like natural language. I think those are great for making developers feel clever but not for people who have to understand and maintain them. I think they have contributed to giving Scala and to a lesser extent Ruby a bad name.


#13

My opinion is that adding an annotation at methods declaration site, in order to allow an infix call of a method, will completely solve any ambiguity on the user code. Maybe a further restriction that the compiler should apply could be that methods could be called with infix syntax only if they are annotatedas infix fun, and in that case they cannot be called with standard syntax. This way, we can avoid the case that someone write a call in infix syntax, and some other in normal syntax, causing confusion.

Eg.:

infix fun Int.to(upper:Int):Any{
...
}

val a=1 to 12
val b= 1.to(12)           //not legal, “to” fun is infix!

fun Int.doStuffWith(aNumber:Int):Any{
  …
}

val a=1 doStuffWith 12   //not legal, doStuffWith fun is not infix!
val b= 1.doStuffWith(12)


#14

Unfortunately you need to be able to use the standard syntax for infix functions to support nullable types

val i: Int? = ... val j = i + 1 // not legal because i could be null val k = i?.plus(1) // legal, if i is null then k is null

If there was another way to solve this I agree it would be good if infix functions could only be called with the infix syntax.


#15

Uhm, you're right I did not think of that...


#16

But we could solve this issue declaring extension method on nullable types, where appropriate:

//I decide that sum on null int instances does not make sense
//so with this definition I avoid the possibility to use sum on Int?

import org.testng.Assert

class MyInt(val value:Int)
class MyString(val value:String)

/infix/ fun MyInt.plus(that:MyInt):MyInt{
  return MyInt(this.value + that.value)
}

//I decide that sum on null String instances make sense.
//(null+stuff)==stuff and (stuff+null)==stuff
//then I use this two definition for sum on String

/infix/ fun MyString.plus(that:MyString?):MyString{
  val thatValue:String = that?.value ?: “”
  return MyString(this.value + thatValue)
}

/infix/ fun MyString?.plus(that:MyString?):MyString{
  val thatValue:String = that?.value ?: “”
  val thisValue:String = this?.value ?: “”
  return MyString(thisValue + thatValue)
}

fun checkPlus():Unit{
  val a:MyString? =null
  val b=MyString(“stuff”)
  val i:MyInt?
  val l=MyInt(12)
  val k:MyInt?=MyInt(10)

  //sum on nullable String
  Assert.assertEquals(a + MyString(“something”),“something”)
  Assert.assertEquals(“something” + a,“something-”)

  //sum on non nullable String
  Assert.assertEquals(b + “something”,“stuff-something”)
  Assert.assertEquals(“something” + b,“somethings-tuff”)
  Assert.assertEquals(b + null,“stuff”)

  //sum on non nullable Int
  Assert.assertEquals(l + 1,14)

  //sum on nullable Int is not allowed (and does not compile)
  //because I decided it does not make sense. So this doesn’t compile
  //Assert.assertEquals(i + 1,12)

  //To sum two nullable Int i must assert that they are not null
  //taking my responsibility as user of sum function
Assert.assertEquals(k!! + 1!!,12)

}


The compiler actually crash on this code, giving a NotSupportedException: "Not enumerate supertypes of Nothing"
But I think it should be compiled fine, and I hope it give the idea.
In this way, there is no need for non-infix call style on plus method, even for nullable,
and only if I code a function that does make sense on null values.
Otherwise, the fun is not defined at all on nullables types.


#17

You are right: there is a tradeoff. What's better for the language: infix calls restricted by annotations or a bunch of new syntactical constructs to compensate for their absence? I'm strongly in favor of the first.


#18

  var btnMinus = Button(this) with {            setText("-")            setOnClickListener(event)            setId(12)   }

Infix function makes pretty code


#19

Is that really so much better than this?

   var btnMinus = Button(this).with {
            setText("-")
            setOnClickListener(event)
            setId(12)
        }

I think the dot makes it clearer what's going on. "with" is obviously a method or extension function of Button. With no dot it's not so clear what it is. Is it a keyword and a built-in language feature? I think it makes the code more ambiguous.

This is my main worry about infix functions. Developers will use them because it makes the code pretty but it also makes it less consistent and more ambiguous.


#20

The . does make a big difference especially in a case where you want the functions to act like a quasi keyword, which is not a bad thing.  I mean Kotlin already encourages DSL development with the optional this, builders, etc.

If we really want to Javanize the language, the snippet should be like below (bring back parentheses, do not make this optional). This will for sure make the whole look and feel of the language consistent.

``

   var btnMinus = Button(this).with(() -> {
           this.setText("-")
           this.setOnClickListener(event)
           this.setId(12)
  })

Map literal will feel different too.
``

val map = hashMap(
    "John".to "Doe",
    "Jane".to "Smith"
)