
مرحبا يا هبر!
اسمي أرتيوم دوبروفينسكي ، أعمل في
فينش . أقترح قراءة مقال لأحد آباء مكتبة البرمجة الوظيفية
Arrow
حول كيفية كتابة برامج متعددة الأشكال. غالبًا ما يكون الأشخاص الذين بدأوا للتو في الكتابة بأسلوب وظيفي غير مستعجلين للتخلي عن العادات القديمة ، وفي الواقع يكتبون حتمية أكثر أناقة قليلاً ، مع حاويات DI ووراثة. إن فكرة إعادة استخدام الوظائف بغض النظر عن الأنواع التي تستخدمها قد تدفع الكثيرين إلى التفكير في الاتجاه الصحيح.
استمتع!
***
ماذا لو استطعنا كتابة التطبيقات دون التفكير في أنواع البيانات التي سيتم استخدامها في وقت التشغيل ، ولكن ببساطة وصف كيفية معالجة هذه البيانات؟
تخيل أن لدينا تطبيقًا يعمل مع النوع القابل للملاحظة من مكتبة RxJava. يسمح لنا هذا النوع بكتابة سلاسل من المكالمات والتلاعب مع البيانات ، ولكن في النهاية ، لن تكون هذه Observable
فقط حاوية بخصائص إضافية؟
نفس القصة مع أنواع مثل Flowable ، و Deferred
(Coroutines) ، و Future
، و IO
، وغيرها الكثير.
من الناحية النظرية ، تمثل كل هذه الأنواع عملية (تم تنفيذها بالفعل أو من المخطط تنفيذها في المستقبل) تدعم عمليات التلاعب مثل تحديد قيمة داخلية لنوع آخر ( map
) ، باستخدام flatMap
لإنشاء سلسلة من العمليات من نفس النوع ، بالإضافة إلى مثيلات أخرى من نفس النوع ( zip
) ، الخ
من أجل كتابة البرامج بناءً على هذه السلوكيات ، مع الحفاظ على الوصف التعريفي ، وكذلك لجعل برامجك مستقلة عن أنواع معينة من البيانات مثل Observable
يكفي أن تتوافق أنواع البيانات المستخدمة مع بعض العقود ، مثل map
و flatMap
وغيرها. .
قد يبدو هذا النهج غريبًا أو معقدًا للغاية ، لكنه يتمتع بمزايا مثيرة للاهتمام. أولاً ، فكر في مثال بسيط ، ثم تحدث عنهم.
مشكلة الكنسي
افترض أن لدينا تطبيقًا له قائمة Task
، ونود استخراج قائمة كائنات من النوع Task
من ذاكرة التخزين المؤقت المحلية. إذا لم يتم العثور عليها في التخزين المحلي ، فسوف نحاول الاستعلام عنها عبر الشبكة. نحتاج إلى عقد واحد لكلا مصدري البيانات حتى يتمكن كلاهما من الحصول على قائمة بالكائنات من النوع Task
لكائن User
مناسب ، بغض النظر عن المصدر:
interface DataSource { fun allTasksByUser(user: User): Observable<List<Task>> }
هنا ، من أجل البساطة ، نرجع إلى " Observable
، ولكن يمكن أن Maybe
Flowable
، Maybe
، Single
Flowable
، Flowable
- أي شيء مناسب لتحقيق الهدف.
أضف اثنين من تطبيقات المخاوي لمصادر البيانات ، واحدة للتطبيق
والآخر
عن
.
class LocalDataSource : DataSource { private val localCache: Map<User, List<Task>> = mapOf(User(UserId("user1")) to listOf(Task("LocalTask assigned to user1"))) override fun allTasksByUser(user: User): Observable<List<Task>> = Observable.create { emitter -> val cachedUser = localCache[user] if (cachedUser != null) { emitter.onNext(cachedUser) } else { emitter.onError(UserNotInLocalStorage(user)) } } } class RemoteDataSource : DataSource { private val internetStorage: Map<User, List<Task>> = mapOf(User(UserId("user2")) to listOf(Task("Remote Task assigned to user2"))) override fun allTasksByUser(user: User): Observable<List<Task>> = Observable.create { emitter -> val networkUser = internetStorage[user] if (networkUser != null) { emitter.onNext(networkUser) } else { emitter.onError(UserNotInRemoteStorage(user)) } } }
تطبيقات كلا مصادر البيانات متطابقة تقريبًا. هذه مجرد إصدارات وهمية من هذه المصادر التي تسحب البيانات من التخزين المحلي أو واجهة برمجة تطبيقات الشبكة. في كلتا الحالتين ، يتم استخدام Map<User, List<Task>>
لتخزين البيانات.
لأن لدينا مصدران للبيانات ، نحتاج إلى تنسيقهما بطريقة أو بأخرى. إنشاء مستودع:
class TaskRepository(private val localDS: DataSource, private val remoteDS: RemoteDataSource) { fun allTasksByUser(user: User): Observable<List<Task>> = localDS.allTasksByUser(user) .subscribeOn(Schedulers.io()) .observeOn(Schedulers.computation()) .onErrorResumeNext { _: Throwable -> remoteDS.allTasksByUser(user) } }
يحاول فقط تحميل List<Task>
من LocalDataSource
، وإذا لم يتم العثور عليها ، فإنه يحاول أن يطلبها من الشبكة باستخدام RemoteDataSource
.
لنقم بإنشاء وحدة بسيطة لتوفير التبعيات دون استخدام أي إطار لحقن التبعية (DI):
class Module { private val localDataSource: LocalDataSource = LocalDataSource() private val remoteDataSource: RemoteDataSource = RemoteDataSource() val repository: TaskRepository = TaskRepository(localDataSource, remoteDataSource) }
وأخيراً ، نحتاج إلى اختبار بسيط يدير مجموعة العمليات بالكامل:
object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val dependenciesModule = Module() dependenciesModule.run { repository.allTasksByUser(user1).subscribe({ println(it) }, { println(it) }) repository.allTasksByUser(user2).subscribe({ println(it) }, { println(it) }) repository.allTasksByUser(user3).subscribe({ println(it) }, { println(it) }) } } }
كل من الكود أعلاه يمكن العثور عليها على جيثب .
يقوم هذا البرنامج بتكوين سلسلة التنفيذ لثلاثة مستخدمين ، ثم الاشتراك في Observable
الناتجة.
أول كائنين من نوع User
متاح ، مع هذا كنا محظوظين. User1
متاح في DataSource
المحلي ، و User2
متوفر في جهاز التحكم عن بعد.
ولكن هناك مشكلة في User3
، لأنه غير متوفر في التخزين المحلي. سيحاول البرنامج تنزيله من خدمة بعيدة - لكنه ليس كذلك أيضًا. سيفشل البحث ، وسنعرض رسالة خطأ في وحدة التحكم.
إليك ما سيتم عرضه في وحدة التحكم للحالات الثلاث:
> [Task(value=LocalTask assigned to user1)] > [Task(value=Remote Task assigned to user2)] > UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user)))
لقد انتهينا من مثال. الآن دعونا نحاول برمجة هذا المنطق بأسلوب
.
نوع البيانات التجريد
الآن سيبدو العقد الخاص بواجهة DataSource
كما يلي:
interface DataSource<F> { fun allTasksByUser(user: User): Kind<F, List<Task>> }
يبدو أن كل شيء متشابه ، ولكن هناك اختلافان مهمان:
- هناك اعتماد على النوع المعمم (عام)
F
- النوع الذي تم إرجاعه بواسطة الوظيفة الآن هو
Kind<F, List<Task>>
.
Kind
هو كيف يقوم السهم بترميز ما يسمى بالنوع (higher kind)
.
سأشرح هذا المفهوم مع مثال بسيط.
Observable<A>
جزأين:
Observable
: حاوية ، نوع ثابت.A
: حجة من النوع العام. التجريد الذي يمكن تمرير أنواع أخرى إليه.
لقد اعتدنا على اتخاذ أنواع عامة مثل A
كما تجريد. لكن لا يعرف الكثير من الناس أنه يمكننا أيضًا تجريد أنواع الحاويات مثل Observable
. لهذا ، هناك أنواع عالية.
والفكرة هي أنه يمكن أن يكون لدينا مُنشئ مثل F<A>
حيث يمكن أن يكون كل من F
و A
نوعًا عامًا. لم يتم اعتماد بناء الجملة هذا من قِبل مترجم Kotlin ( لا يزال؟ ) ، لذلك سنحاكيه بنهج مماثل.
يدعم Arrow هذا من خلال استخدام واجهة meta الوسيطة Kind<F, A>
، التي تحتوي على روابط لكلا النوعين ، كما تقوم بإنشاء محولات في كلا الاتجاهين أثناء التحويل البرمجي بحيث يمكنك اتباع المسار من Kind<Observable, List<Task>>
Observable<List<Task>>
والعكس. ليس حلا مثاليا ، ولكن حلا عمليا.
مرة أخرى ، انظر إلى واجهة مستودعنا:
interface DataSource<F> { fun allTasksByUser(user: User): Kind<F, List<Task>> }
ترجع الدالة DataSource
نوعًا عاليًا: Kind<F, List<Task>>
. إنه يترجم إلى F<List<Task>>
، حيث يظل F
معممًا.
نحن فقط التقاط List<Task>
في التوقيع. بمعنى آخر ، لا يهمنا نوع حاوية F
سيتم استخدامه ، طالما أنه يحتوي على List<Task>
. يمكننا تمرير حاويات بيانات مختلفة إلى الوظيفة. واضح بالفعل؟ المضي قدما.
دعنا نلقي نظرة على DataSource
المطبق بهذه الطريقة ، لكن هذه المرة لكل فرد على حدة. الأول إلى المحلي:
class LocalDataSource<F>(A: ApplicativeError<F, Throwable>) : DataSource<F>, ApplicativeError<F, Throwable> by A { private val localCache: Map<User, List<Task>> = mapOf(User(UserId("user1")) to listOf(Task("LocalTask assigned to user1"))) override fun allTasksByUser(user: User): Kind<F, List<Task>> = Option.fromNullable(localCache[user]).fold( { raiseError(UserNotInLocalStorage(user)) }, { just(it) } ) }
تمت إضافة الكثير من الأشياء الجديدة ، وسوف نقوم بتحليل كل شيء خطوة بخطوة.
يحتفظ DataSource
هذا النوع العام F
لأنه ينفذ DataSource<F>
. نريد الحفاظ على إمكانية نقل هذا النوع من الخارج.
الآن ، ننسى ApplicativeError
غير المألوف في المنشئ والتركيز على وظيفة allTasksByUser()
. وسوف نعود إلى ApplicativeError
.
override fun allTasksByUser(user: User): Kind<F, List<Task>> = Option.fromNullable(localCache[user]).fold( { raiseError(UserNotInLocalStorage(user)) }, { just(it) } )
يمكن ملاحظة أنه يُرجع Kind<F, List<Task>>
. ما زلنا لا نهتم بالحاوية F
طالما أنها تحتوي على List<Task>
.
ولكن هناك مشكلة. اعتمادًا على ما إذا كنا نستطيع العثور على قائمة كائنات Task
للمستخدم المطلوب في التخزين المحلي أم لا ، نريد الإبلاغ عن خطأ (لم يتم العثور على Task
) أو إرجاع Task
تم التفافها بالفعل في F
(تم العثور على Task
).
وفي كلتا الحالتين ، نحتاج إلى العودة: Kind<F, List<Task>>
.
بمعنى آخر: هناك نوع لا نعرف شيئًا عن ( F
) ، ونحن بحاجة إلى طريقة لإرجاع خطأ ملفوف في هذا النوع. بالإضافة إلى ذلك ، نحتاج إلى طريقة لإنشاء مثيل من هذا النوع ، حيث سيتم التفاف القيمة التي تم الحصول عليها بعد الإكمال الناجح للوظيفة. يبدو وكأنه شيء مستحيل؟
دعنا نعود إلى إعلان الفصل ونلاحظ أن ApplicativeError
يتم تمريره إلى المُنشئ ومن ثم استخدامه كمفوض للفئة ( by A
).
class LocalDataSource<F>(A: ApplicativeError<F, Throwable>) : DataSource<F>, ApplicativeError<F, Throwable> by A {
يورث Applicative
من Applicative
، وكلاهما من فئات الكتابة.
تعرف فئات النوع على السلوكيات (العقود). يتم تشفيرها Functor<F>
تعمل مع الوسائط في شكل أنواع عامة ، كما هو الحال في Functor<F>
و Functor<F>
وغيرها الكثير. هذا F
هو نوع البيانات. وبهذه الطريقة ، يمكننا تمرير أنواع مثل Either
، Option
، IO
، Observable
، Flowable
وغيرها الكثير.
لذا ، عد إلى مشكلتنا:
- قم بلف القيمة التي تم الحصول عليها بعد إكمال الوظيفة بنجاح في
Kind<F, List<Task>>
لهذا يمكننا استخدام فئة من نوع Applicative
. بما أن ApplicativeError
موروثة منه ، فيمكننا تفويض خصائصه.
Applicative
يوفر just(a)
وظيفة just(a)
. just(a)
يلتف القيمة في سياق أي نوع عالي. وبالتالي ، إذا كان لدينا Applicative<F>
، فيمكنه استدعاء just(a)
للالتفاف على القيمة في الحاوية F
، أيا كانت تلك القيمة. دعنا Observable
أننا نستخدم Observable
، سيكون لدينا Applicative<Observable>
يعرف كيفية Observable
في Observable
، حتى نحصل على Observable.just(a)
.
- لف الخطأ في المثيل
Kind<F, List<Task>>
لهذا يمكننا استخدام ApplicativeError
. يوفر الدالة raiseError(e)
، التي تلتف الخطأ في حاوية من النوع F
على سبيل المثال ، يمكن إنشاء خطأ ما يشبه Throwable
. خطأ Observable.error<A>(t)
، حيث t
Throwable
، لأننا أعلنا عن نوع الخطأ الخاص بنا كفئة من النوع ApplicativeError<F, Throwable>
.
ألقِ نظرة على تطبيقنا المجرد لـ LocalDataSource<F>
.
class LocalDataSource<F>(A: ApplicativeError<F, Throwable>) : DataSource<F>, ApplicativeError<F, Throwable> by A { private val localCache: Map<User, List<Task>> = mapOf(User(UserId("user1")) to listOf(Task("LocalTask assigned to user1"))) override fun allTasksByUser(user: User): Kind<F, List<Task>> = Option.fromNullable(localCache[user]).fold( { raiseError(UserNotInLocalStorage(user)) }, { just(it) } ) }
تظل Map<User, List<Task>>
المخزنة في الذاكرة كما هي ، لكن الوظيفة تقوم الآن بعدة أشياء قد تكون جديدة بالنسبة لك:
تحاول تحميل قائمة Task
من ذاكرة التخزين المؤقت المحلية ، وبما أن قيمة الإرجاع قد تكون null
(قد لا يتم العثور على Task
) ، فإننا نقوم بتصميم هذا باستخدام Option
. إذا لم يكن من الواضح كيف يعمل Option
، فإنه يصور وجود أو عدم وجود القيمة التي يتم التفافها فيه.
بعد تلقي القيمة الاختيارية ، ندعو fold
فوقه. هذا هو المكافئ لاستخدام when
قيمة اختيارية. إذا كانت القيمة مفقودة ، فإن Option
يلتف الخطأ في نوع البيانات F
(تم تمرير lambda أولاً). وإذا كانت القيمة موجودة ، فإن Option
ينشئ نسخة مجمعة لنوع البيانات F
(امدا الثانية). في كلتا الحالتين ، يتم استخدام خصائص ApplicativeError
المذكورة سابقًا: raiseError()
و just()
.
وبالتالي ، قمنا باستخلاص تطبيق مصادر البيانات باستخدام الفئات بحيث لا يعرفون أي حاوية سيتم استخدامها للنوع F
.
يبدو تطبيق DataSource
للشبكة كما يلي:
class RemoteDataSource<F>(A: Async<F>) : DataSource<F>, Async<F> by A { private val internetStorage: Map<User, List<Task>> = mapOf(User(UserId("user2")) to listOf(Task("Remote Task assigned to user2"))) override fun allTasksByUser(user: User): Kind<F, List<Task>> = async { callback: (Either<Throwable, List<Task>>) -> Unit -> Option.fromNullable(internetStorage[user]).fold( { callback(UserNotInRemoteStorage(user).left()) }, { callback(it.right()) } ) } }
ولكن هناك اختلاف بسيط واحد: بدلاً من التفويض لمثيل ApplicativeError
، نستخدم فئة أخرى مثل: Async
.
هذا لأن مكالمات الشبكة غير متزامنة بطبيعتها. نريد أن نكتب شفرة سيتم تنفيذها بشكل غير متزامن ، فمن المنطقي استخدام فئة كتابة مصممة لهذا الغرض.
يستخدم Async
لمحاكاة العمليات غير المتزامنة. يمكن أن تحاكي أي عملية رد اتصال. لاحظ أننا ما زلنا لا نعرف أنواع البيانات المحددة ؛ فنحن ببساطة نصف عملية غير متزامنة بطبيعتها.
النظر في الوظيفة التالية:
override fun allTasksByUser(user: User): Kind<F, List<Task>> = async { callback: (Either<Throwable, List<Task>>) -> Unit -> Option.fromNullable(internetStorage[user]).fold( { callback(UserNotInRemoteStorage(user).left()) }, { callback(it.right()) } ) }
يمكننا استخدام الدالة async {}
، التي يتم توفيرها لنا مع فئة من النوع Async
لمحاكاة العملية وإنشاء مثيل من النوع Kind<F, List<Task>>
سيتم إنشاؤه بشكل غير متزامن.
إذا استخدمنا نوع بيانات ثابتًا مثل Async.async {}
، Async.async {}
مكافئًا لـ Observable.create()
، أي إنشاء عملية يمكن استدعاؤها من التعليمات البرمجية المتزامنة أو غير المتزامنة ، مثل Thread
أو AsyncTask
.
يتم استخدام المعلمة callback
لربط عمليات الاسترجاعات الناتجة إلى سياق الحاوية F
، وهو نوع عالي.
وبالتالي ، RemoteDataSource
استخراج RemoteDataSource
بنا ويعتمد على الحاوية التي لا تزال غير معروفة من النوع F
دعنا نذهب إلى مستوى التجريد ونلقي نظرة أخرى على مستودعنا. إذا كنت تتذكر ، LocalDataSource
أولاً إلى البحث عن كائنات Task
في LocalDataSource
، وعندها فقط (إذا لم يتم العثور عليها محليًا) لطلبها من RemoteLocalDataSource
.
class TaskRepository<F>( private val localDS: DataSource<F>, private val remoteDS: RemoteDataSource<F>, AE: ApplicativeError<F, Throwable>) : ApplicativeError<F, Throwable> by AE { fun allTasksByUser(user: User): Kind<F, List<Task>> = localDS.allTasksByUser(user).handleErrorWith { when (it) { is UserNotInLocalStorage -> remoteDS.allTasksByUser(user) else -> raiseError(UnknownError(it)) } } }
ApplicativeError<F, Throwable>
معنا مرة أخرى! كما يوفر وظيفة handleErrorWith()
التي تعمل على أعلى أي مستقبل handleErrorWith()
.
يبدو مثل هذا:
fun <A> Kind<F, A>.handleErrorWith(f: (E) -> Kind<F, A>): Kind<F, A>
لأن localDS.allTasksByUser(user)
Kind<F, List<Task>>
، والتي يمكن اعتبارها على أنها F<List<Task>>
، حيث يظل F
نوعًا عامًا ، يمكننا استدعاء handleErrorWith()
فوقه.
handleErrorWith()
يسمح لك بالرد على الأخطاء باستخدام لامدا مرت. دعونا نلقي نظرة فاحصة على الوظيفة:
fun allTasksByUser(user: User): Kind<F, List<Task>> = localDS.allTasksByUser(user).handleErrorWith { when (it) { is UserNotInLocalStorage -> remoteDS.allTasksByUser(user) else -> raiseError(UnknownError(it)) } }
وبالتالي ، نحصل على نتيجة العملية الأولى ، إلا عندما تم طرح استثناء. سيتم معالجة الاستثناء بواسطة lambda. إذا كان الخطأ ينتمي إلى نوع UserNotInLocalStorage
، UserNotInLocalStorage
العثور على كائنات من نوع Tasks
في DataSource
البعيد. في جميع الحالات الأخرى ، نلف الخطأ غير المعروف في حاوية من النوع F
تظل وحدة التبعية مشابهة جدًا للإصدار السابق:
class Module<F>(A: Async<F>) { private val localDataSource: LocalDataSource<F> = LocalDataSource(A) private val remoteDataSource: RemoteDataSource<F> = RemoteDataSource(A) val repository: TaskRepository<F> = TaskRepository(localDataSource, remoteDataSource, A) }
والفرق الوحيد هو أنه أصبح الآن مجردة ويعتمد على F
، الذي لا يزال متعدد الأشكال. لم أكن قد اهتمت بهذا عمدا من أجل تقليل مستوى الضوضاء ، لكن Async
موروث من ApplicativeError
، وبالتالي يمكن استخدامه كمثال له في جميع مستويات تنفيذ البرنامج.
اختبار تعدد الأشكال
أخيرًا ، تطبيقنا مستخلص تمامًا من استخدام أنواع بيانات محددة للحاويات ( F
) ويمكننا التركيز على اختبار التشكل في وقت التشغيل. سنختبر نفس الشفرة ونمرر أنواعًا مختلفة من البيانات إليها للنوع F
السيناريو هو نفسه عندما استخدمنا Observable
.
البرنامج مكتوب بطريقة نتخلص تمامًا من حدود التجريدات ويمكننا توصيل تفاصيل التنفيذ حسب الرغبة.
أولاً ، دعونا نحاول استخدام F
Single
من RxJava كحاوية.
object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val singleModule = Module(SingleK.async()) singleModule.run { repository.allTasksByUser(user1).fix().single.subscribe(::println, ::println) repository.allTasksByUser(user2).fix().single.subscribe(::println, ::println) repository.allTasksByUser(user3).fix().single.subscribe(::println, ::println) } } }
للتوافق ، يوفر Arrow أغلفة لأنواع بيانات المكتبة المعروفة. على سبيل المثال ، هناك غلاف SingleK
مناسب. تتيح لك هذه الأغلفة استخدام فئات الكتابة بالاقتران مع أنواع البيانات كأنواع عالية.
سيتم عرض ما يلي على وحدة التحكم:
[Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user)))
نفس النتيجة ستكون إذا كنت تستخدم Observable
.
الآن دعنا نعمل مع Maybe
، والتي MaybeK
Maybe
هناك:
@JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val maybeModule = Module(MaybeK.async()) maybeModule.run { repository.allTasksByUser(user1).fix().maybe.subscribe(::println, ::println) repository.allTasksByUser(user2).fix().maybe.subscribe(::println, ::println) repository.allTasksByUser(user3).fix().maybe.subscribe(::println, ::println) } }
سيتم عرض النتيجة نفسها على وحدة التحكم ، ولكن الآن باستخدام نوع بيانات مختلف:
[Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user)))
ماذا عن ObservableK
/ FlowableK
؟
لنجربها:
object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val observableModule = Module(ObservableK.async()) observableModule.run { repository.allTasksByUser(user1).fix().observable.subscribe(::println, ::println) repository.allTasksByUser(user2).fix().observable.subscribe(::println, ::println) repository.allTasksByUser(user3).fix().observable.subscribe(::println, ::println) } val flowableModule = Module(FlowableK.async()) flowableModule.run { repository.allTasksByUser(user1).fix().flowable.subscribe(::println) repository.allTasksByUser(user2).fix().flowable.subscribe(::println) repository.allTasksByUser(user3).fix().flowable.subscribe(::println, ::println) } } }
سوف نرى في وحدة التحكم:
[Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) [Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user)))
كل شيء يعمل كما هو متوقع.
دعنا نحاول استخدام DeferredK
، وهو غلاف لنوع kotlinx.coroutines.Deferred
:
object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val deferredModule = Module(DeferredK.async()) deferredModule.run { runBlocking { try { println(repository.allTasksByUser(user1).fix().deferred.await()) println(repository.allTasksByUser(user2).fix().deferred.await()) println(repository.allTasksByUser(user3).fix().deferred.await()) } catch (e: UserNotInRemoteStorage) { println(e) } } } } }
كما تعلمون ، فإن التعامل مع الاستثناء عند استخدام corutin يجب أن يوصف صراحة. , , .
— :
[Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user)))
Arrow API DeferredK
. runBlocking
:
object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val deferredModuleAlt = Module(DeferredK.async()) deferredModuleAlt.run { println(repository.allTasksByUser(user1).fix().unsafeAttemptSync()) println(repository.allTasksByUser(user2).fix().unsafeAttemptSync()) println(repository.allTasksByUser(user3).fix().unsafeAttemptSync()) } } }
[ Try
]({{ '/docs/arrow/core/try/ru' | relative_url }}) (.., Success
Failure
).
Success(value=[Task(value=LocalTask assigned to user1)]) Success(value=[Task(value=Remote Task assigned to user2)]) Failure(exception=UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))))
, , IO
.
IO
, in/out , , .
object test { @JvmStatic fun main(args: Array<String>): Unit { val user1 = User(UserId("user1")) val user2 = User(UserId("user2")) val user3 = User(UserId("unknown user")) val ioModule = Module(IO.async()) ioModule.run { println(repository.allTasksByUser(user1).fix().attempt().unsafeRunSync()) println(repository.allTasksByUser(user2).fix().attempt().unsafeRunSync()) println(repository.allTasksByUser(user3).fix().attempt().unsafeRunSync()) } } }
Right(b=[Task(value=LocalTask assigned to user1)]) Right(b=[Task(value=Remote Task assigned to user2)]) Left(a=UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))))
IO
— . Either<L,R>
( ). , "" Either
, "" , . Right(...)
, , Left(...)
.
.
, . , , , .
.
… ?
, , . .
: , (, ), — . , .
, . . () ( ) , .
(), , (). , .
, . , ( ).
, API . ( map
, flatMap
, fold
, ). , , Kotlin, Arrow — .
DI ( ), .., DI " ". , , . DI, .., , .
, , . , .., , .
بالإضافة إلى ذلك
, .
, , , , .
, . — Twitter: @JorgeCastilloPR .
(, ) :
FP to the max John De Goes FpToTheMax.kt
, arrow-examples
. , , .