TIL that you can use the Invoke Operator on Companion Objects too

12 August 2018

In a previous article, we looked at overloading the invoke() operator, how to do it, and why we might want to.

I was asked to highlight that you can use the invoke() operator on a class’s companion object as a way to have a “validation constructor”.

First of all, cool! I didn’t realise you could do this, but today I learned and it makes sense:

a companion object is initialized when the corresponding class is loaded (resolved), matching the semantics of a Java static initializer.

from the Kotlin Language documentation page on Object Expressions and Declarations.

An initialized companion object is still an instance of a class (of type MyClass.Companion) with which you can overload operators.

Overloading invoke() in a companion object

Let’s see this in action. I haven’t included any third-party libraries, so while the tweet above refers to an Option<User> I’ll just be using Kotlin’s optional type:

class IceCream private constructor(private val scoops: Int) {

    companion object {

        operator fun invoke(): IceCream? {
            return invoke(0)
        }

        operator fun invoke(scoops: Int): IceCream? {
            return if (scoops < 0) {
                null
            } else {
                IceCream(scoops)
            }
        }
    }
}

And what does it look like to use?

val noIceCream = IceCream()
val someIceCream = IceCream(1)
val moreIceCream = IceCream(2)

// or

val invalidIceCream = IceCream(-1)

Should I overload the invoke() operator in a companion object?

I’m not a fan—I would be slightly astonished if I encountered this in a project.

For me, because it looks so much like a constructor invocation, I would expect that these were initializing objects of type IceCream or crashing at runtime because of some validation error inside the constructor.

Are there any hints that the type of these values is not IceCream? Yes!

  • if you explicitly specify the type, or hover over the value, you’ll see that they’re of type IceCream? and not IceCream
  • when you try to use it and get a type error, you’ll realise it’s not IceCream
  • If you hold cmd/ctrl and hover over the () with your mouse, you’ll see it’s clickable and will take you to the declaration of the invoke() operator

So what alternatives are there?

You can use factory classes or even named functions in your companion objects to be more explicit.

I spoke with Paco about these factories returning Option<MyClass> instead of MyClass? or even throwing an exception in case of invalid parameters and he shared:

“This conversation on Twitter highlighted one improvement we could have in our codebases. By moving to validated constructors, we can avoid a whole set of runtime errors.

“These errors would be highly conditional on the parameters on the constructor, which means difficult to trigger while debugging, and would need to be documented and taken into account when refactoring.

“By simply creating a new validated constructor that returns null or an Option (i.e. Arrow/Koptional on a failure), we’re conveying the needs for our object to be constructed, and those will be explicitly mandated across future calls and refactors without requiring users to check the documentation.”

Thanks Paco!