I’d like to discuss the controversial practice of using instance extension functions.
One possible use case might be, when some argument of a method is in some sense fundamental to the operation, one might be tempted to declare it as receiver of the instance method. Doing so may save some boilerplate and arguably make the code easier to read.
Examples of such usage - e.g. when the method actually represents some operation to be applied to some object. Another example - the fundamental argument is some “context object”, such as a writer to which something is being written. Then you may have a family of related methods with this context parameter, and making it a receiver of these operations, eliminates the need to pass this context among them.
Let me demonstrate with an example:
interface Writable {
fun Writer.write()
}
class Node(val title: String, val next: Node?) : Writable {
override fun Writer.write() {
writeTitle() // ok
next?.write() // error
next?.run { write() } // ok
}
private fun Writer.writeTitle() {
write(title)
}
}
This code demonstrates two interesting points.
- you can call instance method with the same receiver, on the same instance (the
writeTitle
method); the receiver object (instance ofWriter
) is automatically dispatched to the method. In other words, the writeTitle method (or its body) has two implicit receivers, one is Node instance, and one is Writer instance, and they can be both correctly dispatched from a code block, that also has these two receivers.
2.you cannot call instance method, on different instance, even if it is the same type as the calling method. So you cannot callnext?.write()
, even if both the calling block, and the called block, have the same receivers. Namely the calling block has the Writer instance as receiver implicitly, and the dispatch command enforces theNode
receiver to be the content of the next variable. But I yould really expect, that the Writer instance would be propagated to the method, in this case.
My first question is - is there some fundamental reason why the method cannot be called directly? Instead, you need to use rather cryptic dispatch: next?.run { write() }
, which creates a code block, where both instances (Writer and Node) are as implicit receivers, and only then you can call the write method.
Related question is - is it a bad idea to use this practice? Apparently reason for not using it is this problem with dispatching calls, but again, is this a fundamental problem? That cannot, or shouldn’t, be fixed in the language?