Search code examples
kotlinkotlin-contracts

Using Kotlin contracts to cast type inside Iterable function predicate


I have this sealed class PictureEvent:

sealed class PictureEvent {

    data class PictureCreated(val pictureId: String, val url: String) : PictureEvent()

    //more classes extending PictureEvent

}

Now, from a list of PictureEvents, I want to get the first PictureCreated:

fun doSomething(events: List<PictureEvent>) {

  val creationEvent = events.first { isCreationEvent(it) } as PictureEvent.PictureCreated

  //do stuff with the creationEvent

}

private fun isCreationEvent(event: PictureEvent) : Boolean {
  return event is PictureEvent.PictureCreated
}

It works fine. As you see, I'm casting the event to PictureCreated (using as keyword), since first method, returns a PictureEvent. I wonder if it's possible to avoid this casting by using Kotlin contracts.

I have tried this:

private fun isCreationEvent(event: PictureEvent) : Boolean {
  contract {
    returns(true) implies (event is PictureEvent.PictureCreated)
  }
  return event is PictureEvent.PictureCreated
}

But it doesn't work; first method keep returning a PictureEvent, instead of PictureCreated. Is it possible to do this currently?


Solution

  • The contract works fine, however if you take a look at the first method signature, you should be able to understand what is happening and why the found object isn't autocast:

    public inline fun <T> Iterable<T>.first(predicate: (T) -> Boolean): T
    

    The return type of the first method is just the same as that defined for all elements in the Iterable instance, PictureEvent in your case, and no autocasting inside the predicate can unfortunately change that.


    Instead of contracts, for example, you can filter your list by the desired class type first, and then take the first element:

    val creationEvent = events
        .filterIsInstance(PictureEvent.PictureCreated::class.java)
        .first()
    

    or create your own extension similar to first:

    inline fun <reified R> Iterable<*>.firstOfInstance(): R {
        val first = first { it is R }
        return first as R
    }
    
    // or wrapping filterIsInstance
    inline fun <reified R> Iterable<*>.firstOfInstance(): R {
        return filterIsInstance(R::class.java).first()
    }
    
    val creationEvent = events.firstOfInstance<PictureEvent.PictureCreated>()