Cara menulis program polimorfik menggunakan Arrow



Halo, Habr!

Nama saya Artyom Dobrovinsky, saya bekerja untuk Finch . Saya sarankan membaca artikel oleh salah satu bapak perpustakaan pemrograman fungsional Arrow tentang cara menulis program polimorfik. Seringkali orang yang baru mulai menulis dengan gaya fungsional tidak terburu-buru berpisah dengan kebiasaan lama, dan bahkan menulis imperatif yang sedikit lebih elegan, dengan wadah DI dan pewarisan. Gagasan menggunakan kembali fungsi terlepas dari jenis yang mereka gunakan mungkin mendorong banyak orang untuk berpikir ke arah yang benar.

Selamat menikmati!


***


Bagaimana jika kita dapat menulis aplikasi tanpa memikirkan jenis data yang akan digunakan dalam runtime, tetapi cukup jelaskan bagaimana data ini akan diproses?


Bayangkan kita memiliki aplikasi yang bekerja dengan tipe Observable dari perpustakaan RxJava. Jenis ini memungkinkan kita untuk menulis rantai panggilan dan manipulasi dengan data, tetapi pada akhirnya, akankah Observable ini tidak hanya menjadi wadah dengan properti tambahan?


Kisah yang sama dengan tipe-tipe seperti Flowable , Deferred (Coroutines), Future , IO , dan banyak lainnya.


Secara konseptual, semua jenis ini mewakili operasi (sudah dilakukan atau direncanakan untuk diimplementasikan di masa depan) yang mendukung manipulasi seperti melemparkan nilai internal ke jenis lain ( map ), menggunakan flatMap untuk membuat rantai operasi dari jenis yang sama, menggabungkan dengan contoh lain dari jenis yang sama ( zip ), dll.


Untuk menulis program berdasarkan perilaku ini, sambil mempertahankan deskripsi deklaratif, dan juga untuk membuat program Anda independen dari tipe data spesifik seperti Dapat Observable cukup bahwa tipe data yang digunakan sesuai dengan kontrak tertentu, seperti map , flatMap , dan lain-lain .


Pendekatan semacam itu mungkin terlihat aneh atau terlalu rumit, tetapi memiliki keuntungan menarik. Pertama, pertimbangkan contoh sederhana, dan kemudian bicarakan.


Masalah kanonik


Misalkan kita memiliki aplikasi dengan to-do list, dan kami ingin mengekstraksi daftar objek bertipe Task dari cache lokal. Jika tidak ditemukan di penyimpanan lokal, kami akan mencoba untuk menanyakannya melalui jaringan. Kami membutuhkan kontrak tunggal untuk kedua sumber data sehingga keduanya bisa mendapatkan daftar objek bertipe Task untuk objek User sesuai, terlepas dari sumbernya:


 interface DataSource { fun allTasksByUser(user: User): Observable<List<Task>> } 

Di sini, untuk kesederhanaan, kami mengembalikan Observable , tetapi dapat berupa Single , Maybe , Flowable , Deferred - apa pun yang cocok untuk mencapai tujuan.


Tambahkan beberapa implementasi sumber data moka, satu untuk dan satu untuk .


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

Implementasi dari kedua sumber data hampir identik. Ini hanyalah versi tiruan dari sumber-sumber ini yang idealnya menarik data dari penyimpanan lokal atau API jaringan. Dalam kedua kasus, Map<User, List<Task>> digunakan untuk menyimpan data.


Karena kami memiliki dua sumber data, kami perlu mengoordinasikannya. Buat repositori:


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

Itu hanya mencoba memuat List<Task> dari LocalDataSource , dan jika tidak ditemukan, ia mencoba untuk meminta mereka dari jaringan menggunakan RemoteDataSource .


Mari kita membuat modul sederhana untuk menyediakan dependensi tanpa menggunakan kerangka kerja untuk injeksi dependensi (DI):


 class Module { private val localDataSource: LocalDataSource = LocalDataSource() private val remoteDataSource: RemoteDataSource = RemoteDataSource() val repository: TaskRepository = TaskRepository(localDataSource, remoteDataSource) } 

Dan akhirnya, kita membutuhkan tes sederhana yang menjalankan seluruh tumpukan operasi:


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

Semua kode di atas dapat ditemukan di github .


Program ini menyusun rantai eksekusi untuk tiga pengguna, kemudian berlangganan Observable dihasilkan.


Dua objek pertama dari tipe User tersedia, dengan ini kami beruntung. User1 tersedia di DataSource lokal, dan User2 tersedia di remote.


Tetapi ada masalah dengan User3 , karena tidak tersedia di penyimpanan lokal. Program akan mencoba mengunduhnya dari layanan jarak jauh - tetapi tidak juga ada di sana. Pencarian akan gagal, dan kami akan menampilkan pesan kesalahan di konsol.


Inilah yang akan ditampilkan di konsol untuk ketiga kasus:


 > [Task(value=LocalTask assigned to user1)] > [Task(value=Remote Task assigned to user2)] > UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) 

Kita selesai dengan sebuah contoh. Sekarang mari kita coba program logika ini dengan gaya .


Abstraksi Tipe Data


Sekarang kontrak untuk antarmuka DataSource akan terlihat seperti ini:


 interface DataSource<F> { fun allTasksByUser(user: User): Kind<F, List<Task>> } 

Semuanya tampak serupa, tetapi ada dua perbedaan penting:


  • Ada ketergantungan pada tipe umum (generik) F
  • Jenis yang dikembalikan oleh fungsi sekarang adalah Kind<F, List<Task>> .

Kind adalah bagaimana Panah mengkodekan apa yang biasa disebut (higher kind) .
Saya akan menjelaskan konsep ini dengan contoh sederhana.


Observable<A> memiliki 2 bagian:


  • Observable : wadah, tipe tetap.
  • A : argumen tipe generik. Sebuah abstraksi yang dapat dilewati oleh tipe lain.

Kita terbiasa mengambil tipe generik seperti A sebagai abstraksi. Tetapi tidak banyak orang yang tahu bahwa kita juga bisa mengabstraksikan jenis wadah seperti Observable . Untuk ini, ada tipe yang tinggi.


Idenya adalah bahwa kita dapat memiliki konstruktor seperti F<A> di mana F dan A dapat menjadi tipe generik. Sintaks ini belum didukung oleh kompiler Kotlin ( masih? ), Jadi kami akan menirunya dengan pendekatan yang sama.


Panah mendukung ini melalui penggunaan antarmuka meta perantara Kind<F, A> , yang berisi tautan ke kedua jenis, dan juga menghasilkan konverter di kedua arah selama kompilasi sehingga Anda dapat mengikuti jalur dari Kind<Observable, List<Task>> ke Observable<List<Task>> dan sebaliknya. Bukan solusi yang ideal, tetapi solusi yang berhasil.


Jadi sekali lagi, lihat antarmuka repositori kami:


 interface DataSource<F> { fun allTasksByUser(user: User): Kind<F, List<Task>> } 

Fungsi DataSource mengembalikan tipe tinggi: Kind<F, List<Task>> . Ini diterjemahkan ke F<List<Task>> , di mana F tetap digeneralisasi.


Kami hanya menangkap List<Task> di tanda tangan. Dengan kata lain, kami tidak peduli jenis wadah F apa yang akan digunakan, asalkan berisi List<Task> . Kami dapat mengirimkan wadah data yang berbeda ke fungsi. Sudah jelas? Silakan.


Mari kita lihat DataSource diimplementasikan dengan cara ini, tetapi kali ini untuk masing-masing individu. Pertama ke lokal:


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

Banyak hal baru telah ditambahkan, kami akan menganalisis semuanya langkah demi langkah.


DataSource ini mempertahankan tipe F generik karena mengimplementasikan DataSource<F> . Kami ingin menjaga kemungkinan mentransmisikan jenis ini dari luar.


Sekarang, lupakan ApplicativeError mungkin tidak dikenal di konstruktor dan fokus pada fungsi allTasksByUser() . Dan kami akan kembali ke ApplicativeError .


 override fun allTasksByUser(user: User): Kind<F, List<Task>> = Option.fromNullable(localCache[user]).fold( { raiseError(UserNotInLocalStorage(user)) }, { just(it) } ) 

Dapat dilihat bahwa ia mengembalikan Kind<F, List<Task>> . Kami masih tidak peduli apa wadah F asalkan berisi List<Task> .


Tapi ada masalah. Bergantung pada apakah kita dapat menemukan daftar objek Task untuk pengguna yang diinginkan di penyimpanan lokal atau tidak, kami ingin melaporkan kesalahan (tidak ada Task ditemukan) atau mengembalikan Task sudah dibungkus dalam F ( Task ditemukan).


Dan untuk kedua kasus, kami harus mengembalikan: Kind<F, List<Task>> .


Dengan kata lain: ada tipe yang tidak kita ketahui tentang ( F ), dan kita perlu cara untuk mengembalikan kesalahan yang terbungkus dalam tipe itu. Plus, kita membutuhkan cara untuk membuat instance dari tipe ini, di mana nilai yang diperoleh setelah fungsi yang berhasil diselesaikan akan dibungkus. Kedengarannya seperti sesuatu yang mustahil?


Mari kita kembali ke deklarasi kelas dan perhatikan bahwa ApplicativeError diteruskan ke konstruktor dan kemudian digunakan sebagai delegasi untuk kelas ( by A ).


 class LocalDataSource<F>(A: ApplicativeError<F, Throwable>) : DataSource<F>, ApplicativeError<F, Throwable> by A { //... } 

ApplicativeError diwarisi dari Applicative , keduanya adalah kelas tipe.


Tipe kelas mendefinisikan perilaku (kontrak). Mereka dikodekan sebagai antarmuka yang bekerja dengan argumen dalam bentuk tipe generik, seperti pada Monad<F> , Functor<F> dan banyak lainnya. F ini adalah tipe data. Dengan cara ini, kita dapat melewatkan tipe seperti Either , Option , IO , Observable , Flowable dan banyak lagi.


Jadi, kembali ke dua masalah kita:


  • Bungkus nilai yang diperoleh setelah fungsi selesai dengan sukses di Kind<F, List<Task>>

Untuk ini, kita dapat menggunakan kelas jenis Applicative . Karena ApplicativeError diwarisi darinya, kami dapat mendelegasikan propertinya.


Applicative hanya menyediakan fungsi just(a) . just(a) membungkus nilai dalam konteks tipe tinggi apa pun. Jadi, jika kita memiliki Applicative<F> , itu bisa memanggil just(a) untuk membungkus nilai dalam wadah F , apa pun nilainya. Katakanlah kita menggunakan Observable , kita akan memiliki Applicative<Observable> yang tahu cara membungkus suatu Observable , sehingga kita mendapatkan Observable.just(a) .


  • Bungkus kesalahan dalam contoh Kind<F, List<Task>>

Untuk ini kita dapat menggunakan ApplicativeError . Ini menyediakan fungsi raiseError(e) , yang membungkus kesalahan dalam wadah tipe F Sebagai contoh yang Observable , kesalahan akan membuat sesuatu seperti Observable.error<A>(t) , di mana t adalah Throwable , karena kami menyatakan jenis kesalahan kami sebagai kelas tipe ApplicativeError<F, Throwable> .


Lihatlah implementasi abstrak dari 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>> tersimpan dalam memori tetap sama, tetapi sekarang fungsinya melakukan beberapa hal yang mungkin baru bagi Anda:


  • Dia mencoba memuat daftar Task dari cache lokal, dan karena nilai kembali mungkin null ( Task mungkin tidak ditemukan), kami memodelkan ini dengan menggunakan Option . Jika tidak jelas cara kerja Option , maka ia memodelkan ada atau tidaknya nilai yang dibungkus di dalamnya.


  • Setelah menerima nilai opsional, kami menyebutnya fold di atasnya. Ini sama dengan menggunakan when atas nilai opsional. Jika nilainya hilang, maka Option membungkus kesalahan dalam tipe data F (lambda pertama berlalu). Dan jika nilainya ada, Option membuat instance wrapper untuk tipe data F (lambda kedua). Dalam kedua kasus, properti ApplicativeError yang disebutkan sebelumnya digunakan: raiseError() dan just() .



Dengan demikian, kami mengabstraksi implementasi sumber data menggunakan kelas sehingga mereka tidak tahu wadah mana yang akan digunakan untuk tipe F .


Menerapkan jaringan DataSource terlihat seperti ini:


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

Tetapi ada satu perbedaan kecil: alih-alih mendelegasikan ke instance ApplicativeError , kita menggunakan kelas lain seperti: Async .


Ini karena panggilan jaringan bersifat tidak sinkron. Kami ingin menulis kode yang akan dieksekusi secara tidak sinkron, adalah logis untuk menggunakan kelas tipe yang dirancang untuk ini.


Async digunakan untuk mensimulasikan operasi asinkron. Itu dapat mensimulasikan operasi panggilan balik. Perhatikan bahwa kita masih tidak tahu tipe data spesifik, kita cukup menggambarkan operasi yang asinkron.


Pertimbangkan fungsi berikut:


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

Kita dapat menggunakan fungsi async {} , yang disediakan kelas tipe Async untuk mensimulasikan operasi dan membuat turunan dari jenis Kind<F, List<Task>> yang akan dibuat secara tidak sinkron.


Jika kami menggunakan tipe data tetap seperti Observable , Async.async {} akan setara dengan Observable.create() , i.e. membuat operasi yang dapat dipanggil dari kode sinkron atau asinkron, seperti Thread atau AsyncTask .


Parameter callback digunakan untuk menghubungkan panggilan balik yang dihasilkan dengan konteks wadah F , yang merupakan tipe tinggi.


Dengan demikian, RemoteDataSource kami diabstraksikan dan tergantung pada wadah tipe F masih belum diketahui F


Mari kita naik ke level abstraksi dan melihat lagi di repositori kami. Jika Anda ingat, pertama-tama kita perlu mencari objek Task di LocalDataSource , dan hanya kemudian (jika mereka tidak ditemukan secara lokal) untuk meminta mereka dari 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> ada bersama kami lagi! Ini juga menyediakan fungsi handleErrorWith() yang bekerja di atas semua penerima kelas atas.


Ini terlihat seperti ini:


 fun <A> Kind<F, A>.handleErrorWith(f: (E) -> Kind<F, A>): Kind<F, A> 

Karena localDS.allTasksByUser(user) mengembalikan Kind<F, List<Task>> , yang dapat dianggap sebagai F<List<Task>> , di mana F tetap menjadi tipe generik, kita dapat memanggil handleErrorWith() di atasnya.


handleErrorWith() memungkinkan Anda untuk merespons kesalahan menggunakan lambda yang diteruskan. Mari kita lihat lebih dekat fungsi:


 fun allTasksByUser(user: User): Kind<F, List<Task>> = localDS.allTasksByUser(user).handleErrorWith { when (it) { is UserNotInLocalStorage -> remoteDS.allTasksByUser(user) else -> raiseError(UnknownError(it)) } } 

Dengan demikian, kami mendapatkan hasil dari operasi pertama, kecuali ketika pengecualian dilemparkan. Pengecualian akan ditangani oleh lambda. Jika kesalahan itu milik tipe UserNotInLocalStorage , kami akan mencoba untuk menemukan objek jenis Tasks di UserNotInLocalStorage jarak jauh. Dalam semua kasus lain, kami membungkus kesalahan yang tidak diketahui dalam wadah tipe F


Modul dependensi tetap sangat mirip dengan versi sebelumnya:


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

Satu-satunya perbedaan adalah bahwa sekarang abstrak dan tergantung pada F , yang tetap polimorfik. Saya sengaja tidak memperhatikan hal ini untuk mengurangi tingkat kebisingan, tetapi Async diwarisi dari ApplicativeError , oleh karena itu ia dapat digunakan sebagai contohnya di semua tingkat pelaksanaan program.


Menguji polimorfisme


Akhirnya, aplikasi kita sepenuhnya diabstraksi dari penggunaan tipe data spesifik untuk container ( F ) dan kita bisa fokus pada pengujian polyformism di runtime. Kami akan menguji bagian kode yang sama dengan meneruskan berbagai jenis data ke tipe F Skenarionya sama dengan ketika kita menggunakan Observable .


Program ini ditulis sedemikian rupa sehingga kami sepenuhnya menghilangkan batasan abstraksi dan dapat menyampaikan detail implementasi yang diinginkan.


Pertama, mari kita coba menggunakan F Single dari RxJava sebagai wadah.


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

Untuk kompatibilitas, Arrow menyediakan pembungkus untuk tipe data perpustakaan yang terkenal. Misalnya, ada pembungkus SingleK praktis. Wrappers ini memungkinkan Anda untuk menggunakan kelas tipe bersama dengan tipe data sebagai tipe tinggi.


Berikut ini akan ditampilkan di konsol:


 [Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) 

Hasil yang sama akan jika menggunakan Observable .


Sekarang mari kita bekerja dengan Maybe , untuk itu MaybeK bungkus MaybeK :


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

Hasil yang sama akan ditampilkan di konsol, tetapi sekarang menggunakan tipe data yang berbeda:


 [Task(value=LocalTask assigned to user1)] [Task(value=Remote Task assigned to user2)] UserNotInRemoteStorage(user=User(userId=UserId(value=unknown user))) 

Bagaimana dengan ObservableK / FlowableK ?
Mari kita coba:


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

Kita akan melihat di konsol:


 [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))) 

Semuanya berfungsi seperti yang diharapkan.


Mari kita coba gunakan DeferredK , pembungkus untuk tipe 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) } } } } } 

Seperti yang Anda ketahui, penanganan pengecualian saat menggunakan corutin harus ditentukan secara eksplisit. , , .


โ€” :


 [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 . , , .

Source: https://habr.com/ru/post/id447234/


All Articles