🏎️ · Operators

5 min read Β· Updated on by

Read on to find a short exposition of operators, and why they should be used sparingly. Special mentions of invoke, componentN, contains and rangeTo, and the index array access operator [].

Unlike Java, Kotlin allows you to provide custom implementations for a predefined set of operators. Operators are ordinary methods that can be called using a special syntax, e.g. operator fun plus() can be called using +== calls equals> calls compareTo etc.

I would recommend you go easy on them. There are situations where implementing an operator makes clear sense (i.e. when creating a Vector2D class, plus makes perfect sense), but unless there is an absolutely crystal clear concept of what a given operator means in the domain you’re modeling, it’s usually best to avoid them.

For example, going back toΒ Vector2D,Β plusΒ might make sense, but what aboutΒ times? There are two product operations associated with a vector, theΒ dotΒ andΒ crossΒ products. Which one should you implement? Probably neither, because there isn’t any general argument that makes one more important or fundamental than the other. But if you don’t implement either, does it make sense to implementΒ plusΒ and be inconsistent with which operations are implemented? It might be, but these are the kinds of questions you should ask yourself before going down this road. Often, it’s much better to simply define a customΒ infix function.

You can find the list of legal operators in the docs. The vast majority won’t surprise you, but there are a couple of operators I want to mention explicitly: invokecomponentNcontains and rangeTo, and the index array access operator [].

Invoke

The invoke operator is certainly among the less traditional operators, because it represents a function call. In actuality, f() calls f.invoke(), in the same way that 2 + 3 actually calls 2.plus(3). You can see this for yourself when you look at the definition of e.g. Function1 and its counterparts.

Therefore, any object can be called like a function just by implementing the invoke operator:


//sampleStart
class ImportantStuffDoer {
    operator fun invoke(x: Int) {
        // do something
    }
}

val instance = ImportantStuffDoer()

val importantStuff = instance.invoke(3) // does important stuff
val alsoImportantStuff = instance(3) // also does important stuff
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the captain on code's sailing ship,
        With extension functions, it doesn't slip.
        From waves to horizons, a journey so wide,
        In the world of development, it's the tide!
    """.trimIndent()
    println(poem)
}

Hopefully this goes without saying, but implementing invoke is not always a good idea! You should think hard before making this decision.

ComponentN

TheΒ componentNΒ family of operators are what enableΒ destructuring, which we talked about in the lesson onΒ data classesΒ (feel free to refresh your memory before moving on).Β When you destructure an expression, what actually happens is thatΒ component1,Β component2Β etc. get called in turn to produce the individual parts of the result:


//sampleStart
// Data classes automatically define as many componentN functions 
// as there are arguments of the primary constructor
data class PersonName(
    val firstName: String, 
    val middleName: String = "", 
    val lastName: String
)

val name = PersonName(firstName = "Mary", lastName = "Jane")

fun destructure() {
    // Destructuring declarations are only allowed for local variables/values
    val (firstName, middleName, lastName) = name
}

// This actually gets compiled to
val firstName = name.component1()
val secondName = name.component2()
val thirdName = name.component3()
//sampleEnd
fun main() {
    val poem = """
        In the coding forest, Kotlin's the guide,
        With extension functions, it walks beside.
        From paths to clearings, a route so fine,
        In the world of programming, it's the sign!
    """.trimIndent()
    println(poem)
}

The same thing happens when you destructure expressions in a lambda:


//sampleStart
data class User(val name: String, val age: Int)
val jane = User("Jane", 35)
fun <T> transformUser(user: User, f: (User) -> T) = f(user)

// Destructuring in lambda parameters
val janeString = transformUser(jane) { (name, age) -> 
    "$name is $age year${ if(age != 1) "s" else "" } old."
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the architect in code's cathedral,
        With extension properties, it's exceptional.
        From arches to domes, a structure so grand,
        In the world of languages, it takes a stand!
    """.trimIndent()
    println(poem)
}

As was mentioned when we talked aboutΒ data classes, you can useΒ _Β to omit a specific component. When you do that, the correspondingΒ componentNΒ method is not called at all:


data class PersonName(
    val firstName: String, 
    val middleName: String = "", 
    val lastName: String
)
//sampleStart
val name = PersonName(firstName = "Mary", lastName = "Jane")

fun destructure() {
    // Destructuring declarations are only allowed for local variables/values
    val (firstName, _, lastName) = name
}

// This actually gets compiler to
val firstName = name.component1()
val thirdName = name.component3()
//sampleEnd
fun main() {
    val poem = """
        When you're in the puzzle of code's maze,
        Kotlin's syntax is the guiding blaze.
        With paths and twists, a journey so vast,
        In the coding labyrinth, it's steadfast!
    """.trimIndent()
    println(poem)
}

Since componentN functions are operators, any class can implement destructuring:


interface ToolBar
interface MainPane
interface IScrolls
//sampleStart
interface Window {
    val toolBar: ToolBar?
    val mainPane: MainPane?
    val verticalBar: IScrolls?
    val horizontalBar: IScrolls?
    
    operator fun component1() = toolBar
    operator fun component2() = mainPane
    operator fun component3() = verticalBar
    operator fun component4() = horizontalBar
}
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the weaver in the coding loom,
        With extension functions, it breaks the gloom.
        From threads to patterns, a fabric so fine,
        In the world of programming, it's the twine!
    """.trimIndent()
    println(poem)
}

Contains & RangeTo

The contains method is called when in is used, and the rangeTo method is called when .. is used. Both can be overridden


//sampleStart
data class Price(val usd: Int) {
    operator fun rangeTo(price: Price): PriceRange = PriceRange(this, price)
}

data class PriceRange(val from: Price, val to: Price) {
    operator fun contains(price: Price) = from.usd <= price.usd && to.usd >= price.usd
}

val `$1` = Price(1)
val `$10` = Price(10)

fun canAfford(cost: Price) = cost in `$1`..`$10`
//sampleEnd
fun main() {
    val poem = """
        In the coding garden, Kotlin's the bloom,
        With extension functions, it brightens the room.
        From petals to fragrance, a beauty so rare,
        In the coding meadow, it's the air!
    """.trimIndent()
    println(poem)
}

However, this is rarely necessary. The standard library contains implementations of these operators for almost all reasonable situations, including numbers, characters, time and date objects, etc. If you do ever find yourself in a situation where you need to implement them, it’s very likely you can just call the builtin implementations:


//sampleStart
data class Price(val usd: Int) {
    operator fun rangeTo(price: Price): PriceRange = PriceRange(usd..price.usd)
}

data class PriceRange(val priceRange: IntRange) {
    operator fun contains(price: Price) = price.usd in priceRange
}

val `$1` = Price(1)
val `$10` = Price(10)

fun canAfford(cost: Price) = cost in `$1`..`$10`
//sampleEnd
fun main() {
    val poem = """
        Kotlin, the explorer in code's frontier,
        With extension properties, it's crystal clear.
        From maps to territories, a journey so wide,
        In the world of programming, it's the guide!
    """.trimIndent()
    println(poem)
}

Indexed array access

Just so you know, it’s there.

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.

The Kotlin Primer