Kotlin Arrow : `Either` مونڈ میں نیویگیشن میں مہارت حاصل کریں
شائع شدہ: 24 September 2024
- تعارف
- Either موناد کیا ہے؟
- سیاق
- مختلف طریقے
- کلاسیکی طریقہ ساتھ `when
- fold` استعمال کر کے ایک زیادہ مختصر رویہ
- تبدéliہ کے ساتھ
mapاور `mapLeft - غلطیوں کا انتظام
getOrElseکے ساتھ - پہلو کی اعمال
onLeftاورonRightکے ساتھ - آپریشنز کی چیننگ
flatMapکے ساتھ - دو طرفہ تبدیلی `bimap
- کناروں کی معکوسی
swapکے ساتھ - استعمال
tapاورtapLeft: - استعمال
recover:
- عملی استعمال کے حالات
- نتیجہ
- مزید آگے جانے
تعارف
Kotlin کی فنگشنل پروگرامنگ کی دنیا میں، آرو لائبریری مسئولیات اور متبادل حالات کو منٹانے کے لیے طاقتور اوزار فراہم کرتی ہے۔ ان اوزاروں میں، مونڈ`Either`اس کی صلاحیت کے طور پر دو ممکن حالات — کامیابی (Right) یا ناکامی (Left) — کو نمایاں کرنے سے یہ ممتاز ہے۔ لیکن ہم ان دو حالات کے درمیان مؤثر طور سے تررد کر سکتے ہیں؟ یہی وہ ہے جو ہم اس مضامین میں جانچنے والے ہیں۔
Either موناد کیا ہے؟
موناد`Either`یہ دو متبادل امکانات کی نمائندگی کرنے والی ایک ڈیٹرسٹرکچر ہے۔ Kotlin میں Arrow کے ساتھ، یہ اکثر ایک آپریشن کے کامیابی اور ناکامی کے حالات کو منیج کرنے کے لیے استعمال ہوتی ہے، جس سے روایتی استثنا کی بجائے ایک خوبصورت متبادل فراہم ہوتی ہے۔
Either کیوں استعمال کریں؟
-
واضح ایرر منیجمنت: ڈویلپر کو ناکامی کے حالات پر غور کرنے کے لیے مجبور کرتا ہے۔
-
فنکشنلی ترکیب: ناكام ہو سکتے ہیں آپریشنز کے چيننگ کو آسان بناتی ہے۔
-
Type-safety: غلطیاں ٹائپڈ ہوتی ہیں، جس سے انہیں مزید واضح انداز میں منظم کرنے میں مدد ملتی ہے۔
-
کسی استثنا نہیں: جانبی اثرات اور تنفيذي بہاؤ کے غیر متوقع انقطاعوں سے بچیں۔
سیاق
چلیں فرض کریں کہ آپ ایک REST API بن رہے ہیںhttps://spring.io/projects/spring-boot/[Spring Boot,ویندوز=پڑھیں بعد] et Kotlin, لائبریری استعمال کرتے ہوئےhttps://arrow-kt.io/[Arrow,وینڈوز=پڑھنے کے بعد]. آپ کے پاس ایک عمل ہے`findOneUserByEmail`جس سے ایک واپس آتا ہے`Either<Throwable, User>`آپ اس نتیجے کو خوبصورت اور مؤثر طریقے سے کس طرح معالجہ کر سکتے ہیں؟
همare pas ek کلاس 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}")
}
}
یہ طریقہ، سادہ اور قابلِ پڑھنے ہونے کے باوجود، Arrow کی فاحشی فنکشنل قابلیت کا پورا فائدہ نہیں اٹھاتا۔
fold` استعمال کر کے ایک زیادہ مختصر رویہ
result.fold(
{ error -> println("Erreur : ${error.message}") },
{ user -> println("Utilisateur trouvé : ${user.login}") }
)
`fold`یہ Left اور Right دونوں صورتوں کے لیے ایک مختصر اور خوبصورت انداز میں اقدامات کی تعریف کرنے کی اجازت دیتا ہے۔
تبدéliہ کے ساتھ 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}")
یہ طریقہ substitute value فراہم کر کے Either.Left کو Either.Right میں تبدیل کر دیتا ہے۔
عملی استعمال کے حالات
-
استعمال کریں`fold`ہر مورد کے لیے سادہ آپریشنز کے لئے علاج کی ضرورت ہوتی ہے - ترجیح دیں`map` et
mapLeft`ڈیٹا کی تبدیلیوں کے لیے، اس کی structurے کو تبدیل کرنے کے بغیر`Either. - انتخاب کریں`flatMap`در حین عملیات کی زنजीری بندی جو ناكام हो सख्ते ہیں - استعمال کریں`recover`خطا کی صورت میں بنیادی مقدار فراہم کرنے کے لیے - چنیں`onLeft` etonRight(outapettapLeft) لوگننگ جیسے جانبدار اثرات کے لیے - استعمال کریں`bimap`دونوں طرفوں کو ایک ہی عمل میں تبدیل کرنے کے لیے - لگو کریں`swap`جب آپ ایک فنکشن کے انٹرーフیس کو دوسرے میں ملازمت کرنا چاہتے ہیں۔
نتیجہ
ان میں سے ہر ایک طریقہ کار کے اپنے فوائد ہیں، جو کہ استعمال کے سیاق و سباق پر منحصر ہیں۔ طریقے جیسے کہ`fold`, map/mapLeft, et `recover`خاص طور پر تب مفید ہوتے ہیں جب آپ کئی آپریشنز کو یکجا کرنا چاہتے ہوں یا ڈیٹا کو فنکشنل طریقے سے تبدیل کرنا چاہتے ہوں۔
موناد`Either`Arrow، آپ کی Kotlin ایپلیکیشنوں میں کامیابی اور غلطی کے حالات کو منظم کرنے میں شاندار لچک فراہم کرتا ہے۔ ان مختلف طریقوں میں مہارت حاصل کر کے، آپ مزید مضبوط، مزید پڑھنے میں آسان اور مزید مؤثر کوڈ لکھ سکتے ہیں۔
اپنے اگلے پروجیکٹ میں، ان تکنیکوں کو ایکسائل کرنے سے ہچکچاہے نہیں تا کہ Kotlin اور Arrow کے ساتھ فنکشنل پروگرامنگ سے سب سے زیادہ فائدہ اٹھایا جا سکے!
مزید آگے جانے
-
آررو کی رسمی دستاویز :https://arrow-kt.io/docs/apidocs/arrow-core/arrow.core/-either/[Arrow Either] - Kotlin Coroutines کے ساتھ Arrow :https://arrow-kt.io/docs/fx/[Arrow Fx Coroutines]
مت بھول اپنی تجربہ اور پسندیدہ تکنیکات کو شیئر کریں`Either`نیچے کے تبصرے میں !