Kotlin / kotlin-style-guide

Work-in-progress notes for the Kotlin style guide
288 stars 14 forks source link

Using infix functions #13

Open yole opened 8 years ago

yole commented 8 years ago

Declare a function as infix only when it works on two objects which play a similar role. Good examples: and, to, zip. Bad example: add.

ilya-g commented 8 years ago

I think, add is a bad example not because its operands are not interchangeable, but because it's an operation mutating one of the operands, instead of returning the result.

For example consider an ImmutableCollection with add method defined as:

interface ImmutableCollection<E> : Collection<E> {
    fun add(element: E): ImmutableCollection<E> 
}

Then the function could be legitimately used as infix in the expression:

val values: ImmutableCollection<String> = ...
val newValues = values add "another"
yole commented 8 years ago

@ilya-g In your example, the "add" method works exactly as the + operator, which is in effect a symmetric operation.

ilya-g commented 8 years ago

plus is not symmetric for collection and element. If you don't like plus, you can take minus/ remove as another example.

damianw commented 8 years ago

I don't think that and is a good candidate. Infix functions should only be used when the operator(s) don't have any implied associativity which could cause confusion when chained together.

val result = w and x or y and z // always left-to-right

On a related note, I think this is a good argument for including bitwise operators in the language, but that's another topic. :)

Groostav commented 8 years ago

I think, add is a bad example not because its operands are not interchangeable, but because it's an operation mutating one of the operands, instead of returning the result.

I support this 100%. All of the mathematics operators typically associated with C-like languages are referentially transparent (ie 'pure'). All mathematics operations, in addition to other symbolic operations like pointer de-referencing and array-index, do not modify the state of the program, instead placing any results or output in their return value.

This was combined with the re-aliasing functionality (read: var functionality) of C and Java so that you could do things like variable += 4, and that represents an interesting collusion of mutability and referential transparency, but at its core I think operators must be referentially transparent.

C#'s += for multicasting delegates was the first counter-example that I regularly used, and for me += got a special place for subscriber/event-registration. This was generalized to mutable maps and lists fairly quickly, such that operators containing an equal sign (eg +=, -=, /=) may modify state, but regular operators (eg +) such be pure, and their corresponding assignment counterpart (eg +=) should only be overloaded with good cause.

I think it should be a firm (if not hard) rule that any infix operator is a pure function, only yielding a result value and not modifying either of its arguments.

Thus, a good operator might be

infix operator fun Path.div(another: Path) = this.resolve(another);

as resolve is a pure function, and both the left and right side of this expression are not mutated.

But something like

infix operator List<Double>.div(scalar: Double) : Unit {
  for(int i : this.indices){
    this[i] = this[i] / scalar
  }
}

would not, since it means the operator typically associated with the pure division function can now modify the contents of lists.