Giriş

hedef kitle : Orta seviyeden ileri seviyeye Kotlin geliştiricileri.

Kotlin’de fonksiyonel programlama dünyasında, Arrow kütüphanesi hataları ve alternatif durumları yönetmek için güçlü araçlar sunar. Bu araçlar arasında, monad`Either`İki olası durumu temsil etme kapasitesiyle ayrılır : başarı (Right) veya başarısızlık (Left). Ancak bu iki durum arasında nasıl etkili bir şekilde geziniriz? Bu, bu makalede keşfedeceğimiz şey.

Either monadı nedir?

Monad`Either`Bir veri yapısıdır ve iki mutlak olarak dışlayıcı olasılığı temsil eder. Kotlin’de Arrow ile, bir işlemin başarı ve başarısızlık durumlarını yönetmek için sıklıkla kullanılır, geleneksel istisnalara şık bir alternatif sunar.

Either’i neden kullanmalıyız?

  1. Açık hata yönetimi: Geliştiriciyi başarısızlık durumlarını düşünmeye zorlar.

  2. Fonksiyonel kompozisyon: Başarısız olabilecek işlemlerin zincirlenmesini kolaylaştırır.

  3. Tip güvenliği: Hatalar tipize edilmiştir, bu da onları daha hassas bir şekilde yönetmeye yardımcı olur.

  4. İstisna yok: Kenar etkileri ve yürütme akışındaki beklenmedik kesintileri kaçının.

Bağlam

Hayal edin ki bir REST API ile geliştiriyorsunuzhttps://spring.io/projects/spring-boot/[Spring Boot,windows=okunacak] et Kotlin, kütüphaneyi kullanarakhttps://arrow-kt.io/[Arrow,windows=sonra-oku]. Bir fonksiyonunuz var.findOneUserByEmail`bir döndüren`Either<Throwable, User>. Bu sonucu nasıl şık ve işlevsel bir şekilde ele alabilirsiniz?

Bir User sınıfımız var:

@file:Suppress(
    "RemoveRedundantQualifierName",
    "MemberVisibilityCanBePrivate",
    "SqlNoDataSourceInspection"
)

package webapp.users

import arrow.core.Either
import arrow.core.left
import arrow.core.right
import com.fasterxml.jackson.annotation.JsonIgnore
import com.fasterxml.jackson.databind.ObjectMapper
import jakarta.validation.constraints.NotNull
import jakarta.validation.constraints.Pattern
import jakarta.validation.constraints.Size
import org.springframework.beans.factory.getBean
import org.springframework.context.ApplicationContext
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate
import org.springframework.r2dbc.core.DatabaseClient
import org.springframework.r2dbc.core.awaitOne
import org.springframework.r2dbc.core.awaitRowsUpdated
import webapp.core.property.ANONYMOUS_USER
import webapp.core.property.EMPTY_STRING
import webapp.core.utils.AppUtils.cleanField
import webapp.users.EntityModel.Companion.ID_MEMBER
import webapp.users.User.UserDao.Attributes.EMAIL_ATTR
import webapp.users.User.UserDao.Attributes.ID_ATTR
import webapp.users.User.UserDao.Attributes.LANG_KEY_ATTR
import webapp.users.User.UserDao.Attributes.LOGIN_ATTR
import webapp.users.User.UserDao.Attributes.PASSWORD_ATTR
import webapp.users.User.UserDao.Attributes.VERSION_ATTR
import webapp.users.User.UserDao.Constraints.LOGIN_REGEX
import webapp.users.User.UserDao.Fields.EMAIL_FIELD
import webapp.users.User.UserDao.Fields.ID_FIELD
import webapp.users.User.UserDao.Fields.LANG_KEY_FIELD
import webapp.users.User.UserDao.Fields.LOGIN_FIELD
import webapp.users.User.UserDao.Fields.PASSWORD_FIELD
import webapp.users.User.UserDao.Fields.VERSION_FIELD
import webapp.users.User.UserDao.Relations.INSERT
import webapp.users.security.Role
import webapp.users.security.Role.RoleDao
import webapp.users.security.UserRole.UserRoleDao
import java.util.*
import jakarta.validation.constraints.Email as EmailConstraint

data class User(
    override val id: UUID? = null,

    @field:NotNull
    @field:Pattern(regexp = LOGIN_REGEX)
    @field:Size(min = 1, max = 50)
    val login: String,

    @JsonIgnore
    @field:NotNull
    @field:Size(min = 60, max = 60)
    val password: String = EMPTY_STRING,

    @field:EmailConstraint
    @field:Size(min = 5, max = 254)
    val email: String = EMPTY_STRING,

    @JsonIgnore
    val roles: MutableSet<Role> = mutableSetOf(Role(ANONYMOUS_USER)),

    @field:Size(min = 2, max = 10)
    val langKey: String = EMPTY_STRING,

    @JsonIgnore
    val version: Long = -1,
) : EntityModel<UUID>() {

    companion object {
        @JvmStatic
        fun main(args: Array<String>) = println(UserDao.Relations.sqlScript)
    }

    object UserDao {
        object Constraints {
            // Regex for acceptable logins
            const val LOGIN_REGEX =
                "^(?>[a-zA-Z0-9!$&*+=?^_`{|}~.-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)|(?>[_.@A-Za-z0-9-]+)$"
            const val PASSWORD_MIN: Int = 4
            const val PASSWORD_MAX: Int = 16
            const val IMAGE_URL_DEFAULT = "https://placehold.it/50x50"
            const val PHONE_REGEX = "^(\\+|00)?[1-9]\\d{0,49}\$"
        }

        object Members {
            const val PASSWORD_MEMBER = "password"
            const val ROLES_MEMBER = "roles"
        }

        object Fields {
            const val ID_FIELD = "`id`"
            const val LOGIN_FIELD = "`login`"
            const val PASSWORD_FIELD = "`password`"
            const val EMAIL_FIELD = "`email`"
            const val LANG_KEY_FIELD = "`lang_key`"
            const val VERSION_FIELD = "`version`"
        }

        object Attributes {
            val ID_ATTR = ID_FIELD.cleanField()
            val LOGIN_ATTR = LOGIN_FIELD.cleanField()
            val PASSWORD_ATTR = PASSWORD_FIELD.cleanField()
            val EMAIL_ATTR = EMAIL_FIELD.cleanField()
            const val LANG_KEY_ATTR = "langKey"
            val VERSION_ATTR = VERSION_FIELD.cleanField()
        }

        object Relations {
            const val TABLE_NAME = "`user`"
            const val SQL_SCRIPT = """
            CREATE TABLE IF NOT EXISTS $TABLE_NAME (
                $ID_FIELD                     UUID default random_uuid() PRIMARY KEY,
                $LOGIN_FIELD                  VARCHAR,
                $PASSWORD_FIELD               VARCHAR,
                $EMAIL_FIELD                  VARCHAR,
                $LANG_KEY_FIELD               VARCHAR,
                $VERSION_FIELD                bigint
            );
            CREATE UNIQUE INDEX IF NOT EXISTS `uniq_idx_user_login`
            ON $TABLE_NAME ($LOGIN_FIELD);
            CREATE UNIQUE INDEX IF NOT EXISTS `uniq_idx_user_email`
            ON $TABLE_NAME ($EMAIL_FIELD);
"""

            @Suppress("SqlDialectInspection")
            const val INSERT = """
            insert into $TABLE_NAME (
                $LOGIN_FIELD, $EMAIL_FIELD,
                $PASSWORD_FIELD, $LANG_KEY_FIELD,
                $VERSION_FIELD
            ) values ( :login, :email, :password, :langKey, :version)"""

            @JvmStatic
            val sqlScript: String
                get() = setOf(
                    UserDao.Relations.SQL_SCRIPT,
                    RoleDao.Relations.SQL_SCRIPT,
                    UserRoleDao.Relations.SQL_SCRIPT
                ).joinToString("")
                    .trimMargin()
        }

        object Dao {
            val Pair<User, ApplicationContext>.toJson: String
                get() = second.getBean<ObjectMapper>().writeValueAsString(first)

            suspend fun Pair<User, ApplicationContext>.save(): Either<Throwable, Long> = try {
                second.getBean<R2dbcEntityTemplate>()
                    .databaseClient
                    .sql(INSERT)
                    .bind(LOGIN_ATTR, first.login)
                    .bind(EMAIL_ATTR, first.email)
                    .bind(PASSWORD_ATTR, first.password)
                    .bind(LANG_KEY_ATTR, first.langKey)
                    .bind(VERSION_ATTR, first.version)
                    .fetch()
                    .awaitRowsUpdated()
                    .right()
            } catch (e: Throwable) {
                e.left()
            }


            suspend fun ApplicationContext.findOneUserByEmail(
                email: String
            ): Either<Throwable, User> = try {
                getBean<DatabaseClient>()
                    .sql("SELECT * FROM `user` WHERE LOWER(email) = LOWER(:email)")
                    .bind("email", email)
                    .fetch()
                    .awaitOne()
                    .let { row ->
                        User(
                            id = row[ID_ATTR] as UUID?,
                            login = row[LOGIN_ATTR] as String,
                            password = row[PASSWORD_ATTR] as String,
                            email = row[EMAIL_ATTR] as String,
                            langKey = row[LANG_KEY_ATTR] as String,
                            version = row[VERSION_ATTR] as Long
                        )
                    }.right()
            } catch (e: Throwable) {
                e.left()
            }
        }
    }

    /** Account REST API URIs */
    object UserRestApis {
        const val API_AUTHORITY = "/api/authorities"
        const val API_USERS = "/api/users"
        const val API_SIGNUP = "/signup"
        const val API_SIGNUP_PATH = "$API_USERS$API_SIGNUP"
        const val API_ACTIVATE = "/activate"
        const val API_ACTIVATE_PATH = "$API_USERS$API_ACTIVATE?key="
        const val API_ACTIVATE_PARAM = "{activationKey}"
        const val API_ACTIVATE_KEY = "key"
        const val API_RESET_INIT = "/reset-password/init"
        const val API_RESET_FINISH = "/reset-password/finish"
        const val API_CHANGE = "/change-password"
        const val API_CHANGE_PATH = "$API_USERS$API_CHANGE"
    }
}

// Abstract entity model with Generic ID, which can be of any type
abstract class EntityModel<T>(
    open val id: T? = null
) {
    companion object {
        const val ID_MEMBER = "id"
    }
}

// Generic extension function that allows the ID to be applied to any EntityModel type
inline fun <reified T : EntityModel<ID>, ID> T.withId(id: ID): T {
    // Use reflection to create a copy with the passed ID
    return this::class.constructors.first { it.parameters.any { param -> param.name == ID_MEMBER } }
        .call(id, *this::class.constructors.first().parameters.drop(1).map { param ->
            this::class.members.first { member -> member.name == param.name }.call(this)
        }.toTypedArray())
}

Farklı yaklaşımlar

Klasik yaklaşım when ile

val user: User by lazy { userFactory(USER) }

val result: Either<Throwable, User> = context.findOneUserByEmail(user.email)

when (result) {
    is Either.Left -> {
        val error = result.value
        println("Erreur : ${error.message}")
    }
    is Either.Right -> {
        val user = result.value
        println("Utilisateur trouvé : ${user.login}")
    }
}

Bu yöntem, basit ve okunabilir olsa da, Arrow’ın fonksiyonel yeteneklerinden tam anlamıyla yararlanmaz.

Kullanım fold için daha özlü bir yaklaşım

result.fold(
{ error -> println("Erreur : ${error.message}") },
{ user -> println("Utilisateur trouvé : ${user.login}") }
)

`fold`İki durum (Left et Right) için eylemleri kısa ve zarif bir şekilde tanımlamaya olanak tanır.

Dönüşüm map ve mapLeft ile

val processedResult = result
    .map { user -> "Utilisateur trouvé : ${user.login}" }
    .mapLeft { error -> "Erreur : ${error.message}" }

println(processedResult.merge())

Bu yaklaşım, Either içindeki değerleri dönüştürmeyi ve yapısını koruyarak sağlar; daha karmaşık işlem zincirleri için idealdir.

Hata yönetimi ile `getOrElse

val user = result.getOrElse { error ->
    println("Erreur : ${error.message}")
    User(login = "default", email = "[email protected]") // utilisateur par défaut
}
println("Login : ${user.login}")

`getOrElse`hata yönetimi şık bir şekilde sunar, varsayılan bir değer sağlayarak

Yan eylemler ile onLeft ve `onRight

result.onLeft { error -> println("Erreur : ${error.message}") }
.onRight { user -> println("Utilisateur trouvé : ${user.login}") }

Bu yöntemler, Either’i değiştirmeden her iki tarafda eylemler gerçekleştirmeye olanak tanır, hafif yan etkiler veya günlük tutma için idealdir.

flatMap` ile işlem zincirleme

fun findUser(email: String): Either<Throwable, User> = // ... implémentation

fun getUserPermissions(user: User): Either<Throwable, List<String>> = // ... implémentation

val userPermissions = findUser("[email protected]")
    .flatMap { user -> getUserPermissions(user) }

flatMap`Bir işi birden fazla işlemle zincirlemek için faydalıdır, kendileri döndüren`Either, böylece önleyerek`Either`iç içe.

İki yönlü dönüşüm bimap ile

val result: Either<Throwable, User> = // ... obtention du résultat
val processedResult = result.bimap(
    { error -> "Erreur: ${error.message}" },
    { user -> "Utilisateur: ${user.login}" }
)

`bimap`sol tarafı ve sağ tarafı tek bir işlemde dönüştürmeye olanak tanır.

swap` ile kenarların ters çevrilmesi

val result: Either<Throwable, User> = // ... obtention du résultat
val swapped = result.swap()

swap`bir şeyin iki tarafını ters çevirmek istediğinizde faydalı olur`Either, bir fonksiyonun arayüzünü başka bir fonksiyonun arayüzüne uyarlamak için, örnek.

Kullanım tap ve tapLeft :

result.tap { user -> println("Utilisateur trouvé : ${user.login}") }
.tapLeft { error -> println("Erreur : ${error.message}") }

benzer`onLeft` et onRight, ancak bu yöntemler orijinal Either’i döndürür, bu da işlemleri zincirleme yapmak için faydalıdır.

Kullanım recover :

val recoveredUser = result.recover { error ->
    println("Erreur récupérée : ${error.message}")
    User(login = "recovered", email = "[email protected]")
}
println("Login : ${recoveredUser.login}")

Bu yöntem, bir yer değiştirme değeri sağlayarak bir Either.Left’i Either.Right’e dönüştürmemizi mümkün kılar.

Pratik kullanım örnekleri

  • Kullanınız`fold`Basit işlemler için her duruma işlem gerektiren. - Tercih edin`map` et mapLeft`içine veri dönüşümleri yapısını değiştirmeden'`Either. - Terçih edin`flatMap`Basarisiz olabilecek islemlerin zincirlemesi sirasinda - Kullanın`recover`bir hata durumunda varsayılan bir değer sağlamak için. - Seçin`onLeft` et onRight (ou tap et tapLeft) yan etkiler gibi log kaydı için. - Kullanin`bimap`iki tarafı tek bir işlem haline getirmek için. - Uygulayın`swap`bir fonksiyonun arayüzünü başka birine uyarlamanız gerektiğinde.

Sonuç

Bu yaklaşımların her biri, kullanım bağlamına göre kendi avantajına sahiptir. Yöntemler gibi`fold`, map/mapLeft, et `recover`birden fazla işlemi zincirlemek veya verileri fonksiyonel bir şekilde dönüştürmek istediğinizde özellikle faydalı olurlar

Monad`Either`d’Arrow, Kotlin uygulamalarınızda başarı ve hata durumlarını yönetmek için esnekliği harika bir şekilde sunar. Bu farklı yaklaşımları öğrenerek, daha sağlam, daha okunabilir ve daha fonksiyonel kod yazabileceksiniz.

Bir sonraki projenizde, Kotlin ve Arrow ile fonksiyonel programlamanın en iyi şekilde faydalanmak için bu teknikleri keşfetmekten çekinmeyin!

Daha ileri gitmek için

  • Resmi Arrow dokümantasyonu :https://arrow-kt.io/docs/apidocs/arrow-core/arrow.core/-either/[Ok Ya] - Kotlin Coroutines ile Arrow :https://arrow-kt.io/docs/fx/[Arrow Fx Korutinler]

Deneyimlerinizi ve tercih ettiğiniz tekniklerinizi paylaşmayı unutmayın, çalışmak için`Either`aşağıdaki yorumlarda !

İlgili Makaleler