https://kotlinlang.org logo
#fp-in-kotlin
Title
# fp-in-kotlin
g

Giovan

09/10/2021, 2:04 PM
Hi guys, I want to ask some questions, my English is poor, but I will attach a code example. Thank you for reading this message. I try code of FP style in kotlin. I use SUM type (
sealed class
) and pattern matching (
when
) to enable functions to match different types to complete different tasks. This makes most of my functions need to do a pattern matching on the type, which makes me feel a little clumsy, and if I need to add a new type, I need to add a case to each of these functions, it is easy to miss the new case. The following is a fictitious code example, assuming it is a sticky note model.
Copy code
sealed class Resource {
    data class Note(val content: String?) : Resource()
    data class Tag(val name: String?) : Resource()
    data class Character(val name: String?) : Resource()
    data class Address(val latitude: Float?, val longitude: Float?) : Resource()
}

fun pullResource(): List<Resource> = TODO()

fun Resource.toDecrypt(): Resource = when (this) {
    is Resource.Note -> TODO()
    is Resource.Tag -> TODO()
    is Resource.Character -> TODO()
    is Resource.Address -> TODO()
}

fun Resource.checkLegal(): Boolean = when (this) {
    is Resource.Note -> content != null
    is Resource.Tag -> name != null
    is Resource.Character -> name != null
    is Resource.Address -> latitude != null && longitude != null
}

inline fun <reified T> List<T>.insert() {
    when (T::class) {
        Resource.Note::class -> TODO("batch insert into the Note Entity")
        Resource.Tag::class -> TODO("batch insert into the Tag Entity")
        Resource.Character::class -> TODO("batch insert into the Character Entity")
        Resource.Address::class -> TODO("batch insert into the Address Entity")
    }
}

fun insertDB(l: List<Resource>) {
    fun <T : Resource> List<Resource>.cast(): List<T> = this as List<T>

    l.groupBy {
        when (it) {
            is Resource.Note -> "Note"
            is Resource.Tag -> "Tag"
            is Resource.Character -> "Character"
            is Resource.Address -> "Address"
        }
    }.let {
        it["Note"]?.cast<Resource.Note>()?.insert()
        it["Tag"]?.cast<Resource.Tag>()?.insert()
        it["Character"]?.cast<Resource.Character>()?.insert()
        it["Address"]?.cast<Resource.Address>()?.insert()
    }
}

fun main() {
    pullResource().map(Resource::toDecrypt).filter(Resource::checkLegal).let(::insertDB)
}
I don’t know if this is right, it feel weird to me. If in OO style, I thought maybe i can define an interface for Resource, and implement the interface when adding cases. The code is clean and does not miss new cases.
j

julian

10/02/2021, 7:57 PM
Hi, @Giovan. Sorry for the late response. I hope you haven't given up on FP 😅! Yes, this sort of pattern is very common in Kotlin FP. I felt the same weirdness/resistance to it when I first started FP. It's normal. I can think of one way to make functions like
insert
and its friends more generic. I hope the following will give you some ideas about further experiments you can try.
Copy code
sealed interface Resource

interface InsertOp<T : Resource> {
   fun insert(resources: List<T>): Resource
}

interface DecryptOp<T : Resource> {
    fun decrypt(resource: Resource): Resource
}

interface ResourceOps<T : Resource> : InsertOp<T>, DecryptOp<T>

fun <T : Resource> List<T>.insert(insertable: InsertOp<T>): List<Resource> {
    insertable.insert(this)
    return this
}

fun <T : Resource> Resource.decrypt(decryptOp: DecryptOp<T>): Resource =
    decryptOp.decrypt(this)

fun <T : Resource> ops(resource: T): ResourceOps<T> = TODO()

fun pullResource(): List<Resource> = TODO()

fun go() {
    pullResource()
        .map { Pair(it, ops(it)) }
        .map { Pair(it.second.decrypt(it.first), it.second) }
        .groupBy { it.second }
        .forEach { entry ->
            entry.key.insert(entry.value.map { it.first })
        }
}
There's boilerplate in
go
that could probably be eliminated with additional abstraction and refactoring.
You said it's easy to miss a case. But actually, it's not, because the compiler requires that your
when
expressions handle all cases. And as of Kotlin
1.5.30
you can also opt in to require exhaustive
when
statements. https://kotlinlang.org/docs/whatsnew1530.html#exhaustive-when-statements-for-sealed-and-boolean-subjects
3 Views