Last updated 2 years ago by Alexander Nozik
kotlinThis article is a follow-up on my previous article on context-oriented programming (I was quite delighted to find that some guys in Kotlin community started to use abbreviation COP for it already). This time I want to go deeper and show a more complicated real-life example of the same approach.
Here is an example that appeared in a discussion about custom mappings. One quite frequently wants for some kind of behavior to be added to the existing class. That is what extensions for:
fun Int.map(): String = toString()
But seldom one wants this behavior to be different in different places. For that, we usually define an interface and few instances like :
interface IntMapper{
operator fun get(index: Int): String
}
object DefaultMapper: IntMapper{
override fun get(index: Int) = "NONE"
}
object MyMapper: IntMapper{
override fun get(index: Int) = index.toString()
}
And then use it wherever we like:
val i = 10
val str = DefaultMapper[i]
It solves the problem in most cases, but it does not, in fact, add behavior to a class. One needs to explicitly call the mapper object each time and it is not what we need. So, since we need some functionality to exist in a context, let us make it context bound:
interface IntMapper{
fun Int.map(): String
}
object MyMapper: IntMapper{
fun Int.map(): String = toString()
}
and then:
MyMapper.run{
val i = 10
val str = i.map()
}
In Kotlin the context is even not necessarily local, one can pass it to some external function, declaring it as a receiver:
fun IntMapper.doSomethingWithMap(){
val i = 10
val str = i.map()
}
MyMapper.run{
doSomethingWithMap()
}
In fact, we can do even better and avoid interfaces completely:
object MapperScope
fun doSomething(){
MapperScope.run{
fun Int.map(): String = toString()
val i = 10
val str = i.map()
}
}
In this case, mapping function will only exist inside one specific scope and will never leave it.