g

    Giovan

    1 year ago
    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.
    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.
    julian

    julian

    11 months ago
    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.
    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