4c84b19b33
#KT-5488 Fixed
150 lines
5.8 KiB
Markdown
150 lines
5.8 KiB
Markdown
# Operator Conventions
|
|
|
|
Kotlin allows us to provide implementations for a predefined set of operators on our types. These operators have fixed symbolic representation
|
|
(like `+` or `*`) and fixed (see grammar). To implement an operator, we provide a member function
|
|
or an extension function with a fixed name, for the corresponding type, i.e. left-hand side type for binary operations and argument type for unary ones.
|
|
|
|
Here we describe the conventions that regulate operator overloading for different operators.
|
|
|
|
## Unary operations
|
|
|
|
| Expression | Translated to |
|
|
|------------|---------------|
|
|
| `+a` | `a.plus()` |
|
|
| `-a` | `a.minus()` |
|
|
| `!a` | `a.not()` |
|
|
|
|
This table says that when the compiler processes, for example, an expression `+a`, it performs the following steps:
|
|
|
|
* Determines the type of `a`, let it be `T`.
|
|
* Looks up a function `plus()` with no parameters for the receiver `T`, i.e. a member function or an extension function.
|
|
* If the function is absent or ambiguous, it is a compilation error.
|
|
* If the function is present and its return type is `R`, the expression `+a` has type `R`.
|
|
|
|
| Expression | Translated to |
|
|
|------------|---------------|
|
|
| `a++` | `a.inc()` + see below |
|
|
| `a--` | `a.dec()` + see below |
|
|
|
|
These operations are supposed to change their receiver and (optionally) return a value.
|
|
|
|
> **`inc()/dec()` shouldn't mutate the receiver object**.<br>
|
|
> By "changing the receiver" we mean _the receiver-variable_, not the receiver object.
|
|
{:.note}
|
|
|
|
The compiler performs the following steps for resolution of an operator in the *postfix* form, e.g. `a++`:
|
|
|
|
* Determines the type of `a`, let it be `T`.
|
|
* Looks up a function `inc()` with no parameters, applicable to the receiver of type `T`.
|
|
* If the function returns a type `R`, then it must be a subtype of `T`.
|
|
|
|
The effect of computing the expression is:
|
|
|
|
* Store the initial value of `a` to a temporary storage `a0`,
|
|
* Assign the result of `a.inc()` to `a`,
|
|
* Return `a0` as a result of the expression.
|
|
|
|
For `a--` the steps are completely analogous.
|
|
|
|
For the *prefix* forms `++a` and `--a` resolution works the same way, and the effect is:
|
|
|
|
* Assign the result of `a.inc()` to `a`,
|
|
* Return the new value of `a` as a result of the expression.
|
|
|
|
## Binary operations
|
|
|
|
| Expression | Translated to |
|
|
| -----------|-------------- |
|
|
| `a + b` | `a.plus(b)` |
|
|
| `a - b` | `a.minus(b)` |
|
|
| `a * b` | `a.times(b)` |
|
|
| `a / b` | `a.div(b)` |
|
|
| `a % b` | `a.mod(b)` |
|
|
| `a..b ` | `a.rangeTo(b)` |
|
|
|
|
For the operations in this table, the compiler just resolves the expression in the *Translated to* column.
|
|
|
|
| Expression | Translated to |
|
|
| -----------|-------------- |
|
|
| `a in b` | `b.contains(a)` |
|
|
| `a !in b` | `!b.contains(a)` |
|
|
|
|
For `in` and `!in` the procedure is the same, but the order of arguments is reversed.
|
|
{:#in}
|
|
|
|
{:#Equals}
|
|
|
|
| Expression | Translated to |
|
|
|------------|---------------|
|
|
| `a == b` | `a?.equals(b) ?: b.identityEquals(null)` |
|
|
| `a != b` | `!(a?.equals(b) ?: b.identityEquals(null))` |
|
|
|
|
*Note*: `===` and `!==` (identity checks) are not overloadable, so no conventions exist for them
|
|
|
|
The `==` operation is special in two ways:
|
|
|
|
* It is translated to a complex expression that screens for `null`'s, and `null == null` is `true`.
|
|
* It looks up a function with a specific _signature_, not just a specific _name_. The function must be declared as
|
|
|
|
``` kotlin
|
|
fun equals(other: Any?): Boolean
|
|
```
|
|
|
|
Or an extension function with the same parameter list and return type.
|
|
|
|
| Symbol | Translated to |
|
|
|--------|---------------|
|
|
| `a > b` | `a.compareTo(b) > 0` |
|
|
| `a < b` | `a.compareTo(b) < 0` |
|
|
| `a >= b` | `a.compareTo(b) >= 0` |
|
|
| `a <= b` | `a.compareTo(b) <= 0` |
|
|
|
|
All comparisons are translated into calls to `compareTo`, that is required to return `Int`.
|
|
|
|
## Indexing and invocations
|
|
|
|
| Symbol | Translated to |
|
|
| -------|-------------- |
|
|
| `a[i]` | `a.get(i)` |
|
|
| `a[i, j]` | `a.get(i, j)` |
|
|
| `a[i_1, ..., i_n]` | `a.get(i_1, ..., i_n)` |
|
|
| `a[i] = b` | `a.set(i, b)` |
|
|
| `a[i, j] = b` | `a.set(i, j, b)` |
|
|
| `a[i_1, ..., i_n] = b` | `a.set(i_1, ..., i_n, b)` |
|
|
|
|
Square brackets are translated to calls to `get` and `set` with appropriate numbers of arguments.
|
|
|
|
| Symbol | Translated to |
|
|
|--------|---------------|
|
|
| `a(i)` | `a.invoke(i)` |
|
|
| `a(i, j)` | `a.invoke(i, j)` |
|
|
| `a(i_1, ..., i_n)` | `a.invoke(i_1, ..., i_n)` |
|
|
|
|
Parentheses are translated to calls to invoke with appropriate number of arguments.
|
|
|
|
## Assignments
|
|
|
|
| Expression | Translated to |
|
|
|------------|---------------|
|
|
| `a += b` | `a.plusAssign(b)` |
|
|
| `a -= b` | `a.minusAssign(b)` |
|
|
| `a *= b` | `a.timesAssign(b)` |
|
|
| `a /= b` | `a.divAssign(b)` |
|
|
| `a %= b` | `a.modAssign(b)` |
|
|
|
|
For the assignment operations, e.g. `a += b`, the compiler performs the following steps:
|
|
|
|
* If the function from the right column is available
|
|
* If the left-hand side can be assigned to and the corresponding binary function (i.e. `plus()` for `plusAssign()`) is available, report error (ambiguity).
|
|
* Make sure its return type is `Unit`, and report an error otherwise.
|
|
* Generate code for `a.plusAssign(b)`
|
|
* Otherwise, try to generate code for `a = a + b` (this includes a type check: the type of `a + b` must be a subtype of `a`).
|
|
|
|
*Note*: assignments are *NOT* expressions in Kotlin.
|
|
|
|
**Discussion of the ambiguity rule**:
|
|
We raise an error when both `plus()` and `plusAssign()` are available only if the lhs is assignable. Otherwise, the availability of `plus()`
|
|
is irrelevant, because we know that `a = a + b` can not compile. An important concern here is what happens when the lhs *becomes assignable*
|
|
after the fact (e.g. the user changes *val* to *var* or provides a `set()` function for indexing convention): in this case, the previously
|
|
correct call site may become incorrect, but not the other way around, which is safe, because former calls to `plusAssign()` can not be silently
|
|
turned into calls to `plus()`. |