परिचय

लक्षित दर्शक : मध्यम से उन्नत स्तर के कोटलिन डेवलपर्स

Kotlin में कार्यात्मक प्रोग्रामिंग की दुनिया में, Arrow लाइब्रेरी शक्तिशाली उपकरण प्रदान करती है जो त्रुटियों और वैकल्पिक मामलों को संभालने के लिए हैं। इन उपकरणों में, मोनैड`Either`यह अपनी दो संभावित स्थितियों को दर्शाने की क्षमता के कारण विशेष है: सफलता (Right) या विफलता (Left)। लेकिन हम इन दो स्थितियों के बीच प्रभावी रूप से नेविगेट कैसे करें? ये हम इस लेख में खोजने वाले हैं।

इथर मोनैड क्या है ?

एकक`Either`यह दो आपस में अनन्य संभावनाओं का प्रतिनिधित्व करने वाला एक डेटा संरचना है। Kotlin में Arrow के साथ, यह अक्सर किसी ऑपरेशन की सफलता और विफलता के मामलों को प्रबंधित करने के लिए उपयोग की जाती है, पारंपरिक अपवादों का एक शानदार विकल्प प्रदान करती है।

Either का उपयोग क्यों करें?

  1. स्पष्ट त्रुटि प्रबंधन : डेवलपर को असफलता के मामलों पर विचार करने के लिए बाध्य करता है।

  2. कार्यात्मक संयोजन: असफल हो सकने वाले ऑपरेशनों की श्रृंखलाकरण को सुविधा प्रदान करता है।

  3. प्रकार‑सुरक्षा: त्रुटियाँ प्रकारबद्ध होती हैं, जिससे उनका अधिक सटीक प्रबंधन करना संभव होता है।

  4. Pas d’exceptions : Évite les effets de bord et les interruptions inattendues du flux d’exécution.

संदर्भ

मान लीजिए कि आप एक REST API विकसित कर रहे हैंhttps://spring.io/projects/spring-boot/[Spring Boot,विंडोज़=पढ़ें बाद में] et Kotlin, पुस्तकालय का उपयोग करकेhttps://arrow-kt.io/[तीर,विंडोज़=पढ़ें-बाद में]. आपके पास एक फ़ंक्शन है`findOneUserByEmail`जो एक लौटाता है`Either<Throwable, User>`आप इस परिणाम को शालीन और कार्यात्मक ढंग से कैसे संभाल सकते हैं?

हमारे पास एक User क्लास है:

@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())
}

विभिन्न दृष्टिकोण

पारंपरिक पद्धति के साथ `when

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}")
    }
}

हालांकि यह विधि सरल और पठनीय है, यह तीर की कार्यात्मक क्षमताओं का पूरी तरह से उपयोग नहीं करती।

fold` का उपयोग एक अधिक संक्षिप्त दृष्टिकोण के लिए

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

`fold`यह Left और Right दोनों मामलों के लिए कार्यों को परिभाषित करने की अनुमति देता है, संक्षिप्त और शालीन तरीके से।

परिवर्तन map और mapLeft के साथ

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

println(processedResult.merge())

यह दृष्टिकोण Either में निहित मान को बदलने की अनुमति देता है जबकि उसकी संरचना को संरक्षित रखता है, जो अधिक जटिल प्रक्रिया श्रृंखलाओं के लिए आदर्श है।

त्रुटि प्रबंधन के साथ `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`यह एक शानदार त्रुटि प्रबंधन प्रदान करता है क्योंकि यह एक डिफ़ॉल्ट मान प्रदान करने की अनुमति देता है।

साइड कार्रवाई onLeft और onRight के साथ

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

ये तरीके किसी भी तरफ कार्रवाई करने की अनुमति देते हैं, Either को संशोधित किए बिना, लॉगिंग या हल्के दुष्प्रभाव के लिए उपयुक्त।

ऑपरेशन चेनिंग flatMap के साथ

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`यह ऐसे कार्यों को क्रम में जोड़ने के लिए उपयोगी है जो स्वयं लौटाते हैं`Either, इस प्रकार बचते हुए`Either`संलग्न.

द्विदिशात्मक परिवर्तन के साथ `bimap

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

`bimap`बायें और दायें दोनों पक्षों को एक ही ऑपरेशन में बदलने की अनुमति देता है।

swap के साथ किनारों को उलटना

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

swap`यह उपयोगी है जब आप एक पक्ष को उलटना चाहते हैं`Either, उदाहरण के लिए एक फ़ंक्शन के इंटरफ़ेस को दूसरे इंटरफ़ेस के अनुसार अनुकूलित करने के लिए.

tap` और tapLeft का उपयोग:

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

जैसे`onLeft` et onRight, लेकिन इन विधियों में मूल Either लौटाया जाता है, जो संचालन की श्रृंखला के लिए उपयोगी है।

उपयोग recover :

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

यह विधि एक प्रतिस्थापन मान प्रदान करके Either.Left को Either.Right में बदलने की अनुमति देती है।

प्रायोगिक उपयोग के मामले

  • उपयोग करें`fold`सरल संचालन के लिए जो प्रत्येक मामले के लिए प्रसंस्करण की आवश्यकता होती है। - आप प्राथमिकता दें`map` et mapLeft`डेटा परिवर्तनों के लिए बिना संरचना बदले de l'`Either. चुनें`flatMap`चेनिंग के दौरान विफल हो सकने वाले संचालन के दौरान उपयोग करें`recover`त्रुटि के मामले में एक डिफ़ॉल्ट मान प्रदान करने के लिए - चुनें`onLeft` et onRight (ou tap et tapLeft) साइड इफ़ेक्ट्स जैसे लॉगिंग के लिए. - उपयोग करें`bimap`दोनों पक्षों को एक ही ऑपरेशन में बदलने के लिए - लागू करें`swap`जब आपको किसी फ़ंक्शन के इंटरफ़ेस को दूसरे से अनुकूलित करने की आवश्यकता होती है।

निष्कर्ष

इनमें से प्रत्येक दृष्टिकोण के अपने फायदे होते हैं उपयोग के संदर्भ के अनुसार। तरीके जैसे`fold`, map(No output)mapLeft, et `recover`वे विशेष रूप से उपयोगी होते हैं जब आप कई ऑपरेशन को क्रम में रखना चाहते हैं या डेटा को कार्यात्मक रूप से बदलना चाहते हैं।

मोनैड`Either`d’Arrow कोट्लिन एप्लिकेशनों में सफलता और त्रुटि के मामलों को संभालने के लिए उल्लेखनीय लचीलापन प्रदान करता है। इन विभिन्न दृष्टिकोनों को समझने पर, आप अधिक मजबूत, पढ़ने में आसान और अधिक कार्यात्मक कोड लिख पाएँगे।

अपने अगले प्रोजेक्ट में, इन तकनीकों का अन्वेषण करने में संकोच न करें ताकि आप कोटलिन और एरो के साथ कार्यात्मक प्रोग्रामिंग से अधिकतम लाभ उठा सकें !

और आगे जाने के लिए

  • एरो की आधिकारिक दस्तावेज़:https://arrow-kt.io/docs/apidocs/arrow-core/arrow.core/-either/[तीर या] # Gradle एकीकरण JBake को JBake Gradle प्लगिन का उपयोग करके या JBake CLI को सीधे बुलाकर Gradle बिल्ड में एकीकृत किया जा सकता है:

----
tasks.register<JavaExec>("bake") {
    mainClass.set("org.jbake.launcher.Main")
    classpath = configurations["jbake"]
    args = listOf(projectDir.absolutePath, "$buildDir/jbake")
}https://arrow-kt.io/docs/fx/[तीर Fx कोरूटीन]

अपने अनुभव और पसंदीदा तकनीकें साझा करना न भूलें साथ में काम करने के लिए`Either`नीचे दिए गए टिप्पणियों में !
----

संबंधित लेख