Wie man polymorphe Programme mit Arrow schreibt



Hallo Habr!

Mein Name ist Artyom Dobrovinsky, ich arbeite für Finch . Ich schlage vor, einen Artikel eines der Väter der Arrow Bibliothek für funktionale Programmierung über das Schreiben polymorpher Programme zu lesen. Oft haben Menschen, die gerade erst anfangen, in einem funktionalen Stil zu schreiben, es nicht eilig, sich von alten Gewohnheiten zu trennen, und schreiben tatsächlich einen etwas eleganteren Imperativ mit DI-Containern und Vererbung. Die Idee, Funktionen unabhängig von den verwendeten Typen wiederzuverwenden, kann viele dazu veranlassen, in die richtige Richtung zu denken.

Viel Spaß!


***.


Was wäre, wenn wir Anwendungen schreiben könnten, ohne über die Datentypen nachzudenken, die zur Laufzeit verwendet werden, aber einfach beschreiben, wie diese Daten verarbeitet werden?


Stellen Sie sich vor, wir haben eine Anwendung, die mit dem Observable Typ aus der RxJava-Bibliothek funktioniert. Dieser Typ ermöglicht es uns, Ketten von Aufrufen und Manipulationen mit Daten zu schreiben. Wird dieses Observable am Ende nicht nur ein Container mit zusätzlichen Eigenschaften sein?


Die gleiche Geschichte mit Typen wie Flowable , Deferred (Coroutines), Future , IO und vielen anderen.


Konzeptionell stellen alle diese Typen eine Operation dar (die bereits ausgeführt wurde oder in Zukunft implementiert werden soll), die Manipulationen wie das flatMap eines internen Werts in einen anderen Typ ( map ) unterstützt. Mit flatMap eine Kette von Operationen eines ähnlichen Typs erstellt und mit anderen Instanzen desselben Typs ( zip kombiniert ), usw.


Um Programme zu schreiben, die auf diesen Verhaltensweisen basieren, während eine deklarative Beschreibung beibehalten wird, und um Ihre Programme unabhängig von bestimmten Datentypen wie Observable aus, dass die verwendeten Datentypen bestimmten Verträgen wie map , flatMap und anderen entsprechen .


Ein solcher Ansatz mag seltsam oder zu kompliziert erscheinen, hat aber interessante Vorteile. Betrachten Sie zunächst ein einfaches Beispiel und sprechen Sie dann darüber.


Kanonisches Problem


Angenommen, wir haben eine Anwendung mit einer Task und möchten eine Liste von Objekten vom Typ Task aus dem lokalen Cache extrahieren. Wenn sie nicht im lokalen Speicher gefunden werden, werden wir versuchen, sie über das Netzwerk abzufragen. Wir benötigen einen einzigen Vertrag für beide Datenquellen, damit beide eine Liste von Objekten vom Typ Task für ein geeignetes User , unabhängig von der Quelle:


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

Der Einfachheit halber geben wir hier Observable , aber es kann Single , Maybe , Flowable , Deferred - alles, was zur Erreichung des Ziels geeignet ist.


Fügen Sie einige Mokka-Implementierungen von Datenquellen hinzu, eine für und eine für .


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

Die Implementierungen beider Datenquellen sind nahezu identisch. Dies sind einfach Scheinversionen dieser Quellen, die idealerweise Daten aus dem lokalen Speicher oder der Netzwerk-API abrufen. In beiden Fällen wird Map<User, List<Task>> zum Speichern von Daten verwendet.


Weil Wir haben zwei Datenquellen, wir müssen sie irgendwie koordinieren. Erstellen Sie ein Repository:


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

Es wird lediglich versucht, die List<Task> von LocalDataSource zu laden. Wenn sie nicht gefunden wird, wird versucht, sie mithilfe von RemoteDataSource vom Netzwerk RemoteDataSource .


Erstellen wir ein einfaches Modul zum Bereitstellen von Abhängigkeiten, ohne ein Framework für die Abhängigkeitsinjektion (DI) zu verwenden:


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

Und schließlich brauchen wir einen einfachen Test, der den gesamten Stapel von Operationen ausführt:


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

Der gesamte obige Code befindet sich auf dem Github .


Dieses Programm erstellt die Ausführungskette für drei Benutzer und abonniert dann das resultierende Observable .


Die ersten beiden Objekte vom Typ User sind verfügbar, damit hatten wir Glück. User1 ist in der lokalen DataSource verfügbar, und User2 ist in der Fernbedienung verfügbar.


Es gibt jedoch ein Problem mit User3 , da es nicht im lokalen Speicher verfügbar ist. Das Programm wird versuchen, es von einem Remote-Dienst herunterzuladen - aber es ist auch nicht da. Die Suche schlägt fehl und es wird eine Fehlermeldung in der Konsole angezeigt.


In allen drei Fällen wird Folgendes in der Konsole angezeigt:


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

Wir sind mit einem Beispiel fertig. Versuchen wir nun, diese Logik im Stil des zu programmieren.


Datentypabstraktion


Der Vertrag für die DataSource Schnittstelle sieht nun folgendermaßen aus:


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

Alles scheint ähnlich zu sein, aber es gibt zwei wichtige Unterschiede:


  • Es besteht eine Abhängigkeit vom generalisierten Typ (generisch) F
  • Der von der Funktion zurückgegebene Typ ist jetzt Kind<F, List<Task>> .

Kind ist, wie Arrow das codiert, was üblicherweise als (higher kind) .
Ich werde dieses Konzept anhand eines einfachen Beispiels erläutern.


Observable<A> besteht aus 2 Teilen:


  • Observable : Behälter, fester Typ.
  • A : Argument eines generischen Typs. Eine Abstraktion, an die andere Typen übergeben werden können.

Wir sind es gewohnt, generische Typen wie A als Abstraktionen zu verwenden. Aber nicht viele Leute wissen, dass wir auch Containertypen wie Observable abstrahieren können. Dafür gibt es hohe Typen.


Die Idee ist, dass wir einen Konstruktor wie F<A> in dem sowohl F als auch A ein generischer Typ sein können. Diese Syntax wird vom Kotlin-Compiler noch nicht unterstützt ( noch? ), Daher werden wir sie mit einem ähnlichen Ansatz nachahmen.


Arrow unterstützt dies durch die Verwendung einer Zwischen-Meta-Schnittstelle Kind<F, A> , die Links zu beiden Typen enthält und während der Kompilierung Konverter in beide Richtungen generiert, sodass Sie dem Pfad von Kind<Observable, List<Task>> folgen können Kind<Observable, List<Task>> zu Observable<List<Task>> und umgekehrt. Keine ideale Lösung, aber eine funktionierende.


Schauen Sie sich also noch einmal die Oberfläche unseres Repositorys an:


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

Die DataSource Funktion gibt einen hohen Typ zurück: Kind<F, List<Task>> . Es wird in F<List<Task>> , wobei F verallgemeinert bleibt.


Wir erfassen nur die List<Task> in der Signatur. Mit anderen Worten, es ist uns egal, welcher Container vom Typ F verwendet wird, solange er eine List<Task> . Wir können verschiedene Datencontainer an die Funktion übergeben. Schon klar? Mach weiter.


DataSource wir einen Blick auf die auf diese Weise implementierte DataSource , diesmal jedoch für jede einzelne. Zuerst zu 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) } ) } 

Es wurden viele neue Dinge hinzugefügt, wir werden alles Schritt für Schritt analysieren.


Diese DataSource behält den generischen Typ F da sie eine DataSource<F> implementiert. Wir wollen die Möglichkeit behalten, diesen Typ von außen zu übertragen.


Vergessen Sie nun den möglicherweise unbekannten ApplicativeError im Konstruktor und konzentrieren Sie sich auf die Funktion allTasksByUser() . Und wir werden zu ApplicativeError .


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

Es ist ersichtlich, dass Kind<F, List<Task>> . Es ist uns immer noch egal, was der Container F , solange er eine List<Task> .


Aber es gibt ein Problem. Abhängig davon, ob wir die Liste der Task für den gewünschten Benutzer im lokalen Speicher finden können oder nicht, möchten wir einen Fehler melden (keine Task gefunden) oder die bereits in F Task ( Task gefunden).


In beiden Fällen müssen wir Folgendes zurückgeben: Kind<F, List<Task>> .


Mit anderen Worten: Es gibt einen Typ, von dem wir nichts wissen ( F ), und wir brauchen eine Möglichkeit, einen in diesen Typ eingeschlossenen Fehler zurückzugeben. Außerdem benötigen wir eine Möglichkeit, eine Instanz dieses Typs zu erstellen, in die der nach erfolgreichem Abschluss der Funktion erhaltene Wert eingeschlossen wird. Klingt nach etwas Unmöglichem?


Kehren wir zur Klassendeklaration zurück und stellen fest, dass ApplicativeError an den Konstruktor übergeben und dann als Delegat für die Klasse ( by A ) verwendet wird.


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

ApplicativeError von Applicative geerbt, beide sind Typklassen.


Typklassen definieren Verhaltensweisen (Verträge). Sie sind als Schnittstellen codiert, die mit Argumenten in Form generischer Typen arbeiten, wie in Monad<F> , Functor<F> und vielen anderen. Dieses F ist ein Datentyp. Auf diese Weise können wir Typen wie Either , Option , Flowable , Observable , Flowable und viele mehr übergeben.


Zurück zu unseren beiden Problemen:


  • Wickeln Sie den Wert, der nach erfolgreichem Abschluss der Funktion erhalten wurde, in Kind<F, List<Task>>

Hierfür können wir eine Klasse vom Typ Applicative . Da ApplicativeError von ihm geerbt wird, können wir seine Eigenschaften delegieren.


Applicative bietet just(a) Funktion just(a) . just(a) umschließt den Wert im Kontext eines beliebigen High-Typs. Wenn wir also Applicative<F> , kann es just(a) aufrufen, um den Wert in Container F zu verpacken, unabhängig davon, um welchen Wert es sich handelt. Nehmen wir an, wir verwenden Observable . Wir haben ein Applicative<Observable> , das weiß, wie man ein Observable , damit wir Observable.just(a) .


  • Schließen Sie den Fehler in die Instanz Kind<F, List<Task>>

Hierfür können wir ApplicativeError . Es bietet eine Funktion raiseError(e) , die den Fehler in einen Container vom Typ F raiseError(e) Für das Observable Beispiel erzeugt ein Fehler so etwas wie Observable.error<A>(t) , wobei t Throwable , da wir unseren Fehlertyp als Klasse vom Typ ApplicativeError<F, Throwable> .


Schauen Sie sich unsere abstrakte Implementierung von 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) } ) } 

Die im Speicher gespeicherte Map<User, List<Task>> bleibt unverändert, aber jetzt führt die Funktion einige Dinge aus, die für Sie möglicherweise neu sind:


  • Sie versucht, die Task aus dem lokalen Cache zu laden. Da der Rückgabewert möglicherweise null ( Task möglicherweise nicht gefunden), modellieren wir dies mithilfe der Option . Wenn nicht klar ist, wie Option funktioniert, wird das Vorhandensein oder Fehlen des darin eingeschlossenen Werts modelliert.


  • Nachdem wir den optionalen Wert erhalten haben, rufen wir fold auf. Dies entspricht der Verwendung der when für einen optionalen Wert. Wenn der Wert fehlt, Option den Fehler mit dem Datentyp F (erstes Lambda übergeben). Wenn der Wert vorhanden ist, erstellt Option eine Wrapper-Instanz für den Datentyp F (zweites Lambda). In beiden Fällen werden die zuvor genannten ApplicativeError Eigenschaften verwendet: raiseError() und just() .



Daher haben wir die Implementierung von Datenquellen mithilfe von Klassen abstrahiert, damit diese nicht wissen, welcher Container für den verwendeten Typ F .


Implementieren einer Netzwerk- DataSource sieht folgendermaßen aus:


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

Es gibt jedoch einen kleinen Unterschied: Anstatt an die ApplicativeError Instanz zu delegieren, verwenden wir eine andere Klasse wie: Async .


Dies liegt daran, dass Netzwerkanrufe asynchroner Natur sind. Wir möchten Code schreiben, der asynchron ausgeführt wird. Es ist logisch, eine dafür vorgesehene Typklasse zu verwenden.


Async verwendet, um asynchrone Operationen zu simulieren. Es kann jeden Rückrufvorgang simulieren. Beachten Sie, dass wir die spezifischen Datentypen immer noch nicht kennen. Wir beschreiben lediglich eine asynchrone Operation.


Betrachten Sie die folgende Funktion:


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

Wir können die Funktion async {} , für die wir eine Klasse vom Typ Async , um die Operation zu simulieren und eine Instanz vom Typ Kind<F, List<Task>> erstellen, die asynchron erstellt wird.


Wenn wir einen festen Datentyp wie Observable , wäre Async.async {} äquivalent zu Observable.create() , d. H. Erstellen einer Operation, die aus synchronem oder asynchronem Code wie Thread oder AsyncTask .


Der callback wird verwendet, um die resultierenden Rückrufe mit dem Containerkontext F zu verknüpfen, der ein hoher Typ ist.


Somit ist unsere RemoteDataSource abstrahiert und hängt vom noch unbekannten Container vom Typ F


Gehen wir zur Abstraktionsebene und werfen einen weiteren Blick auf unser Repository. Wenn Sie sich erinnern, müssen wir zuerst in LocalDataSource nach Task Objekten LocalDataSource und erst dann (wenn sie nicht lokal gefunden wurden), um sie von 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> ist wieder bei uns! Es bietet auch eine handleErrorWith() -Funktion, die auf jedem High-End-Empfänger funktioniert.


Es sieht so aus:


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

Weil localDS.allTasksByUser(user) gibt Kind<F, List<Task>> , was als F<List<Task>> , wobei F ein generischer Typ bleibt. handleErrorWith() können wir handleErrorWith() aufrufen.


handleErrorWith() können Sie mit dem übergebenen Lambda auf Fehler reagieren. Schauen wir uns die Funktion genauer an:


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

Somit erhalten wir das Ergebnis der ersten Operation, außer wenn eine Ausnahme ausgelöst wurde. Die Ausnahme wird vom Lambda behandelt. Wenn der Fehler zum UserNotInLocalStorage Typ gehört, werden wir versuchen, Objekte vom Typ Tasks in der Remote- DataSource . In allen anderen Fällen verpacken wir den unbekannten Fehler in einen Container vom Typ F


Das Abhängigkeitsmodul bleibt der vorherigen Version sehr ähnlich:


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

Der einzige Unterschied besteht darin, dass es jetzt abstrakt ist und von F abhängt, das polymorph bleibt. Ich habe dies bewusst nicht beachtet, um den Rauschpegel zu reduzieren, aber Async von ApplicativeError geerbt, daher kann es auf allen Ebenen der Programmausführung als Instanz verwendet werden.


Polymorphismus testen


Schließlich ist unsere Anwendung vollständig von der Verwendung bestimmter Datentypen für Container ( F ) abstrahiert, und wir können uns darauf konzentrieren, den Polyformismus zur Laufzeit zu testen. Wir werden denselben Code testen, der verschiedene Datentypen für Typ F an ihn F Das Szenario ist das gleiche wie bei der Verwendung von Observable .


Das Programm ist so geschrieben, dass wir die Grenzen von Abstraktionen vollständig beseitigen und Implementierungsdetails nach Wunsch übermitteln können.


Versuchen wir zunächst, F Single aus RxJava als Container zu verwenden.


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

Aus Kompatibilitätsgründen bietet Arrow Wrapper für bekannte Bibliotheksdatentypen. Zum Beispiel gibt es einen praktischen SingleK Wrapper. Mit diesen Wrappern können Sie Typklassen in Verbindung mit Datentypen als hohe Typen verwenden.


Folgendes wird auf der Konsole angezeigt:


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

Das gleiche Ergebnis wird Observable , wenn Observable .


Lassen Sie uns nun mit Maybe , für das der MaybeK Wrapper 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) } } 

Das gleiche Ergebnis wird auf der Konsole angezeigt, verwendet jedoch jetzt einen anderen Datentyp:


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

Was ist mit ObservableK / FlowableK ?
Probieren wir es aus:


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

Wir werden in der Konsole sehen:


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

Alles funktioniert wie erwartet.


Versuchen wir es mit DeferredK , einem Wrapper für den Typ 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) } } } } } 

Wie Sie wissen, muss die Ausnahmebehandlung bei Verwendung von Corutin ausdrücklich vorgeschrieben werden. , , .


— :


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


  • , , . , .., , .



Optional


, .
, , , , .


, . — Twitter: @JorgeCastilloPR .


(, ) :



FP to the max John De Goes FpToTheMax.kt , arrow-examples . , , .

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


All Articles