Cleaner self returning functions

Although the DSL features in Kotlin mostly rendered builder pattern needless, there are some cases when self returning functions have their cause. A self returning function is a function returning the scoped object itself and used for fluent API-s. An often used pattern:

class X {
    private var y : T

    fun withY(value: T) : X {
        y = value
        return this        
   }
}

Then one can use these functions:

val x = X().withY(v).withZ().withA(a)

I know, that a very similar effect could be achieved by apply or also, but there are situations when that is not as fluent as the above form, and shortly I will show other limitations of them.

Returning to the original idea, the traditional syntax have boilercode (return this) and it is not clear in the API syntax whether the function returns the same object or a copy (like in immutable classes). With this syntax, it may be simplified and made clean. A possible example for the syntax could be:

fun withY(value: T) : this { y = value }

At first, it looks like it gives little gain over

fun withY(value: T) = this.apply { y = value }

but it shorten the code and makes it explicit that the returned object is the same. Also it could render polymorphically: an inherited class would safely return its static class even when the function isn’t overridden in it.

Let’s see the following example:

open class P {
    fun a() = this.apply { println("Setting a") }
}

class C : P() {
    fun b() = this.apply { println("Setting b") }
}

fun main() {
    C().a().b()  // Won't compile
}

This is a pitfall of builder pattern over inheritance. Solving this usually leads to a quite ugly code involving self-refered generics, necessary, but meaningless abstract classes and lots of suppressed cast warnings:

abstract class A<R : A<R>> {
    @Suppress("UNCHECKED_CAST")
    fun a() = this.apply { println("Setting a") } as R
}

// An "empty" class for hiding the implementation-only generic
class P : A<P>()

// C logically inherits from P, but it is not visible any more
class C : A<C>() {
    fun b() = this.apply { println("Setting b") }
}

fun main() {
    P().a()      // Compile
    P().a().b()  // Won't compile
    C().a().b()  // Compile
}

With the suggested solution, the code would clean up to:

open class P {
    fun a() : this { println("Setting a") } 
}

class C : P() {
    fun b() : this { println("Setting b") }
}

fun main() {
    C().a().b()  // Compile
}

As a conclusion, my suggestion would give the following benefits:

  • Slightly shorter and cleaner code by itself
  • Explicit API declaring the returned object is not a copy, but the object itself
  • Could be “auto polymorphic”: the compiler knows the exact type of the function call, it could safely return that one (the above cast is meant to be safe and implicit and the generic parameter is not needed).

I am looking forward your opinion!

Balage

Put this wherever you want in your project:

operator inline fun <T> T.invoke(action: T.() -> Unit): T =
    apply(action)

Now you can do:

class Foo {
    fun a(): Foo = this { print("Hello Foo") }
}

Make it protected in your class so that only sub-classes can use it:

open class Foo {
    protected operator inline fun <T> T.invoke(action: T.() -> Unit): T =
        apply(action)

    fun a(): Foo = this { print("Hello Foo") }
}
3 Likes

I thought I liked this idea at first… but upon reflection I think a method contract that promises to return exactly what the caller already has is a pretty silly idea.

We only put up with this pattern, because it makes fluent APIs. It would be better just to add better language support for fluent calling syntax, which also/apply already does.

2 Likes

First I’ve tried to argue, but I have to admit, you are right. I have no heavy arguments and you may be right that my way of thinking about fluent APIs should be adapted to the tools already there. Using fluent API is a little more to my liking when used within another function call (a personal opinion), but as I think of it, even I could give more arguments pro apply/also.

:slight_smile:

Sometimes it is better (although often not easier) to adopt our thinking to the environment than changing the environment to match our thinking.

2 Likes

Also if you make it inline no contract is needed!

1 Like