Defining an Operator Invoke Extension Function on Kotlin Function Types
18 Aug 2022I’ve been producing a lot of YouTube Videos lately. They’re about refactoring and TDD and all that good stuff - you really should subscribe.
In one episode I have a class that takes a function type property in its constructor. The property defines a strategy for how to update items.
Here I’m going to go off on the tangent that Jekyll’s home-page generation fails if you have a code block that is split on the front page. So this text is just to move this code block down the page:
class Stock(
private val stockFile: File,
private val zoneId: ZoneId,
private val updater: (items: List<Item>, days: Int, on: LocalDate) -> List<Item>
) {
fun stockList(now: Instant): Result4k<StockList, StockListLoadingError> =
stockFile.loadItems().flatMap { stockList: StockList ->
val daysOutOfDate = stockList.lastModified.daysTo(now, zoneId).toInt()
when {
daysOutOfDate > 0 -> update(stockList, updater, now, daysOutOfDate, zoneId).savedTo(stockFile, now)
else -> Success(stockList)
}
}
}
private fun update(
stockList: StockList,
updater: (items: List<Item>, days: Int, on: LocalDate) -> List<Item>,
now: Instant,
daysOutOfDate: Int,
zoneId1: ZoneId
): StockList = stockList.copy(
lastModified = now,
items = updater(stockList.items, daysOutOfDate, LocalDate.ofInstant(now, zoneId1))
)
So a Stock
is an object that can load a StockList
from a file, updating the stock Item
s using a strategy
defined by the updater
function. It’s all a bit of a tangle, yes, but this is the middle of a refactor.
The nastiness of that update
function is because updater
has the wrong signature as far this code is concerned.
We need it to work on a StockList
, but it is defined on List<Item>
because that’s a simpler interface for our
clients.
So here’s a ‘cute’ trick. Take the updater
parameter to update
and make it the receiver.
...
daysOutOfDate > 0 -> updater.update(stockList, now, daysOutOfDate, zoneId).savedTo
...
private fun ((items: List<Item>, days: Int, on: LocalDate) -> List<Item>).update(
stockList: StockList,
now: Instant,
daysOutOfDate: Int,
zoneId1: ZoneId
): StockList = stockList.copy(
lastModified = now,
items = this(stockList.items, daysOutOfDate, LocalDate.ofInstant(now, zoneId1))
)
Now convert it to an operator fun
invoke
extension
...
daysOutOfDate > 0 -> updater.invoke(stockList, now, daysOutOfDate, zoneId).savedTo(stockFile, now)
...
private operator fun ((items: List<Item>, days: Int, on: LocalDate) -> List<Item>).invoke(
stockList: StockList,
now: Instant,
daysOutOfDate: Int,
zoneId1: ZoneId
): StockList = stockList.copy(
lastModified = now,
items = this(stockList.items, daysOutOfDate, LocalDate.ofInstant(now, zoneId1))
)
and finally remove the vestigial invoke
:
class Stock(
private val stockFile: File,
private val zoneId: ZoneId,
private val updater: (items: List<Item>, days: Int, on: LocalDate) -> List<Item>
) {
fun stockList(now: Instant): Result4k<StockList, StockListLoadingError> =
stockFile.loadItems().flatMap { stockList: StockList ->
val daysOutOfDate = stockList.lastModified.daysTo(now, zoneId).toInt()
when {
daysOutOfDate > 0 -> updater(stockList, now, daysOutOfDate, zoneId).savedTo(stockFile, now)
else -> Success(stockList)
}
}
}
private operator fun ((items: List<Item>, days: Int, on: LocalDate) -> List<Item>).invoke(
stockList: StockList,
now: Instant,
daysOutOfDate: Int,
zoneId1: ZoneId
): StockList = stockList.copy(
lastModified = now,
items = this(stockList.items, daysOutOfDate, LocalDate.ofInstant(now, zoneId1))
)
So what we have is an operator invoke function defined as an extension on a function type.
This way we have ‘seamlessly’ (well, there is a seam, but it is invisible) converted our updater
from the function we
have to the function we want.
To be honest, I’m pretty sure that the benefit is not worth the cleverness here, but I present the technique on the off-chance that it digs you out of a hole somewhere. If it does, please let me know on Twitter.