When to use extension functions and properties

Please consider this:

A) The String class in Kotlin has only 5 members (functions / properties). What are the reasons it is so small?
Instead, it could have a lot more methods, like contentEquals, contains, dropLastWhile, decapitalize, toBoolean, toIntOrNull, toLowerCase.

B)

data class MyExClass(
    val a: Int,
    val b: String,
    val c: Int
) {
    fun myFunAsMember(param1: Long) = a > param1 && c < param1 && b.length > 2
}

fun MyExClass.myFunAsExtension(param1: Long) = a > param1 && c < param1 && b.toBoolean()length > 2

When should I create an extension function as myFunAsExtension or a member function as myFunAsMember?

I’m asking this because I want to be able to help some Java developers better understand Kotlin and its way of going about things, and I’ll start by explaining when they should use extension functions and properties. As Java developers, they only want to use extension functions when they need to modify an existing library whose source code cannot be changed. They also aren’t comfortable in seeing functions defined outside a class.

1 Like

Hi there,

If I remember this year “put down the golden hammer” talk at kotlin conf, there was a part about using extension function to build utility features on top of a “pure” and minimalist API. That being said I find this approach a bit extreme, someone else might explain it better.

In my case I rely on extension function for helpers method on classes that I do not control and I sometimes use them on my own classes when the method I want to create only uses existing public methods.

As a lot of things in Kotlin, the use of extension function depends a lot on your own coding style.

1 Like

Classes should do one thing and one thing only.
The thing data-classes do is giving access to the data that is stored.
Utility functions should deal with making the interaction easier.
Maybe that’s the reason why String is composed this way?

I personnally use extension-functions for a couple of reasons:

  1. To make things fluent:
    You could make private functions in your class that do something to parameters as part of some bigger logic:
    doSomethingWith(a : A), operateOn(b : B), applySomethingTo(c : C).
    I like to replace those functions with private functions that operate directly on those parameters:
    A.doSomething(), B.operate(), C.applySomething

  2. To let data-classes be data-classes and don’t have any logic.

    data class Account(val id : Int, val userName: String, val passWord: String)
    data class Car(val id : Int, val brand : String)
    data class CarRental(val accountId: Int, val carId: Int){
         companion object
    }
    infix fun Account.rent(car : Car) = CarRental(id, car.id)
    fun main(){
        val lease = Account(...) rent Car(..)
    }
    //or
    data class CarRental.Companion.from(car : Car, account : Account){
        CarRental(car, account)
    }
    fun main(){
        val lease = CarRental(Car(), Account())
    }
    

    The data-classes don’t know anything about eachother and so, they can be changed individually.

    You could use operator CarRental.Companion.invoke() to create a fake constructor, but this is the same as creating a top level function with the name CarRental().

  3. In typesafe builders. You can use the unaryplus for adding some classes to a list. see typesafe builders for an example.
    There are two ways to implement a DSL: You can add a function that creates a class and immediately stores the value or you could create a DSL that has functions that create classes which in turn you have to add to the DSL. The second way allows you to create classes outside of the lamdba which you will add later on in the DSL using the unaryPlus (when you do forget to do that, You just created a class and threw it away. Therefor it’s important to know what type of DSL you use. I use both, where lowercase adds immediately and uppercase you have to add yourself).

  4. Adding Reified functions to interfaces.
    Interfaces cannot have inline functions. Therefor, these need to be defined outside of the interface.

    interface Parser{
        fun <T> parse(
            toParse: String,
            nullable: Boolean,
            clazz : Class<out T>
        ) : T
    }
    inline fun <reified T> Parser.parse(
        toParse: String
    ) = parse(toParse, null is T, T::class.java)
    
  5. To interact with other libraries, but I almost never interact with java-libs anymore…

In my library KotlinPoetDSL (don’t use, as it’s far behind KotlinPoet) I use interfaces as a way to create a micro-architecture and add a lot of functions by using extension-functions. In this framework, I abuse kotlin, but I use the pattern itself reasonably often if I want to create an extendable architecture.

4 Likes

There is one problem with extensions. They always have lower priority than members. An API author can break source-level compatibility by simply adding a new member without changing the old ones (because someone uses an extension with the same name). This makes more difficult to maintain strict backward compatibility of Kotlin APIs compared to Java.

1 Like

Doesn’t the same issue exist with member functions?

Say there’s a library interface:

interface Foo {
    fun a(): String
}

You implement it:

class MyFoo : Foo {
    override fun a(): String = "a"

    fun b(): String = "b"
}

Then later on the library developer adds a new function to Foo:

interface Foo {
    fun a(): String

    fun b(): Int {
        return 2 
    }
}
1 Like

Doesn’t the same issue exist with member functions?

New implementation will not compile since Kotlin, unlike Java, requires to mark all overrides with the keyword. In the case of extensions, there will be no compilation error or warning.

2 Likes

Even ignoring the override keyword, It wouldn’t work in Java because of the different, incompatible return types

Extension functions can also be used to implement functionality for other classes in a specific scope:

object ParserContext {
    inline fun <reified T> String.parse(): T { ... }
}

fun main() {
    val str = "example"
    val number: Int

    with(ParserContext) {
        number = str.parse()
    }
    
    println(number)
}

If a class requires extended functionality only in a specific scope and does not access members that are required to be private, i would prefer to use scope-limited extension functions as above to limit pollution.

Regarding added functionality potentially name-shadowing extension functions, i would opt for extension functions having the same name and signature as members being a compiler error.

EDIT :
Of course, my example was only written with the idea to showcase scoped extension functions in mind, and it might not always be the way to go to create objects and with scopes just to escape namespace pollution :sweat_smile:

However, should the scope have actual state, a with(foo) { } or foo.run { } might already be present and extension functions might make things more comfortable, or lambda parameters might be scoped to a class or object that implements extension functions.

Also, class private extension functions are great to provide idiomatic access to functionality that is only required inside one class, but at multiple points, without repeating yourself.

2 Likes

In the case of String, there are some good reasons why extension functions are used. String is not the fundamental class you think it is. In reality, the CharSequence interface (which String implements) is the more fundamental type. It just has 3 things, a length a way to get individual characters and a way to get a subsequence. Many of those methods you mention are implemented for both CharSequence and for String. They could have made them part of the CharSequence interface and provided default implementations there and then String would override them. But now you have made those methods virtual methods in the CharSequence hierarchy where they take up resources in some virtual method table for every implementation of CharSequence and you are allowing someone else to implement CharSequence and to override those methods to have different meanings.

2 Likes