Search code examples
kotlinjava-streamoption-type

What's the Idiomatic Way in Kotlin to Use Streams with map and orElseThrow?


I am transitioning from Java to Kotlin and am used to working with Java Streams. I have the following two Kotlin snippets that essentially do the same thing: they attempt to create a retrospective through a client call and map it to a model. If the creation or mapping fails, an exception should be thrown.

Solution 1:

val createdRetro = 
retroClient.createRetro(newRetroDto)?.let(retroMapper::toModel)
    ?: throw Exception("Could not create retro")

Solution 2:

val createdRetro2 = 
sequenceOf(retroClient.createRetro(newRetroDto))
    .filterNotNull()
    .map { retroMapper.toModel(it) }
    .firstOrNull() ?: throw Exception("Could not create retro")

The second approach seems more familiar to me coming from Java, where I often used Streams with map and orElseThrow.

Which of these is more idiomatic in Kotlin? Is there a more Kotlin-native approach that combines the readability and safety features I'm looking for?


Solution

  • I would certainly not create a sequence for this, so option 2 is out of the question IMO, unless you do want to process multiple elements.

    The first option is ok, but it would be even better without let (for my personal taste), and this is probably due to the fact that the conventions are not respected here. X.toY() should convert an X to a Y. It's strange to have X.toY(Z) that converts Z to Y. So I would rather redefine the signature of toModel so the receiver is the thing that is converted. That actually plays much nicer in chains:

    val createdRetro = retroClient.createRetro(newRetroDto)?.toModel(retroMapper)
        ?: throw Exception("Could not create retro")
    

    You could even remove retroMapper here by defining the extension toModel in a scope that has access to the mapper.

    Note that you could also simply fail early without changing anything else:

    val rawCreatedRetro = retroClient.createRetro(newRetroDto)
        ?: throw Exception("Could not create retro")
    val createdRetro = retroMapper.toModel(rawCreatedRetro)
    

    IMO the signature that you chose for toModel encourages this kind of style instead.