Defining an Operator Invoke Extension Function on Kotlin Function Types

I’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 Items 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.

[ If you liked this, you could share it on Twitter. ]