Einführung in Arrow in Kotlin

Einführung in Arrow in Kotlin

1. Überblick

Arrow ist eine Bibliothek, die ausKΛTEGORY undfunKTionale zusammengeführt wurde.

In diesem Tutorial werden wir uns mit den Grundlagen von Arrow befassen und wie es uns helfen kann, die Leistungsfähigkeit der funktionalen Programmierung in Kotlin zu nutzen.

Wir werden die Datentypen im Kernpaket diskutieren und einen Anwendungsfall zur Fehlerbehandlung untersuchen.

2. Maven-Abhängigkeit

Um Arrow in unser Projekt aufzunehmen, müssen wirthe arrow-core dependency hinzufügen:


    io.arrow-kt
    arrow-core
    0.7.3

3. Funktionsdatentypen

Beginnen wir mit der Untersuchung der Datentypen im Kernmodul.

3.1. Einführung in Monaden

Einige der hier diskutierten Datentypen sind Monaden. Monaden haben grundsätzlich folgende Eigenschaften:

  • Sie sind ein spezieller Datentyp, der im Grunde ein Wrapper um einen oder mehrere Rohwerte ist

  • Sie haben drei öffentliche Methoden:

    • Eine Factory-Methode zum Umbrechen von Werten

    • map

    • flatMap

  • Diese Methoden wirkennicely, dh sie haben keine Nebenwirkungen.

In der Java-Welt sind Arrays und Streams Monaden, aberOptional isn’t. Für mehr über Monaden vielleicht einbag of peanuts can help.

Sehen wir uns nun den ersten Datentyp aus dem Modularrow-corean.

3.2. Id

Id ist der einfachste Wrapper in Arrow.

Wir können es mit einem Konstruktor oder mit einer Factory-Methode erstellen:

val id = Id("foo")
val justId = Id.just("foo");

Und es hat eineextract-Methode, um den umschlossenen Wert abzurufen:

Assert.assertEquals("foo", id.extract())
Assert.assertEquals(justId, id)

Die KlasseIderfüllt die Anforderungen des Monadenmusters.

3.3. Option

Option ist ein Datentyp zum Modellieren eines Werts, der möglicherweise nicht vorhanden ist, ähnlich wie bei Java Optional.

Und obwohl es technisch gesehen keine Monade ist, ist es dennoch sehr hilfreich.

Es kann zwei Typen enthalten: DerSome -Swrapper um den Wert oderNone, wenn er keinen Wert hat.

Wir haben verschiedene Möglichkeiten, einOptionzu erstellen:

val factory = Option.just(42)
val constructor = Option(42)
val emptyOptional = Option.empty()
val fromNullable = Option.fromNullable(null)

Assert.assertEquals(42, factory.getOrElse { -1 })
Assert.assertEquals(factory, constructor)
Assert.assertEquals(emptyOptional, fromNullable)

Now, there is a tricky bit here,, dh die Factory-Methode und der Konstruktor verhalten sich fürnull unterschiedlich:

val constructor : Option = Option(null)
val fromNullable : Option = Option.fromNullable(null)
Assert.assertNotEquals(constructor, fromNullable)

Wir bevorzugen die zweite, da sie keinenKotlinNullPointerException -Strisk hat:

try {
    constructor.map { s -> s!!.length }
} catch (e : KotlinNullPointerException) {
    fromNullable.map { s -> s!!.length }
}

3.3. Either

Wie wir zuvor gesehen haben, könnenOption entweder keinen Wert (None) oder einen Wert (Some). haben

Either geht weiter auf diesem Weg und kann einen von zwei Werten haben. Either hat zwei generische Parameter für den Typ der beiden Werte, die alsright undleft: bezeichnet werden

val rightOnly : Either = Either.right(42)
val leftOnly : Either = Either.left("foo")

Diese Klasse ist aufright-biased ausgelegt. Der richtige Zweig sollte also den Geschäftswert enthalten, beispielsweise das Ergebnis einer Berechnung. Der linke Zweig kann eine Fehlermeldung oder sogar eine Ausnahme enthalten.

Daher ist die Wertextraktionsmethode (getOrElse) auf der rechten Seite ausgelegt:

Assert.assertTrue(rightOnly.isRight())
Assert.assertTrue(leftOnly.isLeft())
Assert.assertEquals(42, rightOnly.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.getOrElse { -1 })

Sogar die Methodenmap undflatMap funktionieren mit der rechten Seite und überspringen die linke Seite:

Assert.assertEquals(0, rightOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertEquals(-1, leftOnly.map { it % 2 }.getOrElse { -1 })
Assert.assertTrue(rightOnly.flatMap { Either.Right(it % 2) }.isRight())
Assert.assertTrue(leftOnly.flatMap { Either.Right(it % 2) }.isLeft())

In Abschnitt 4 wird untersucht, wieEither für die Fehlerbehandlung verwendet werden.

3.4. Eval

Eval ist eine Monade zur Steuerung der Bewertung von Operationen. It has a built-in support for memoization and eager and lazy evaluation.

Mit der Factory-Methodenowkönnen wir aus bereits berechneten Werten eineEval-Instanz erstellen:

val now = Eval.now(1)

Die Operationenmap undflatMap werden träge ausgeführt:

var counter : Int = 0
val map = now.map { x -> counter++; x+1 }
Assert.assertEquals(0, counter)

val extract = map.value()
Assert.assertEquals(2, extract)
Assert.assertEquals(1, counter)

Wie wir sehen können, ändert sichcounter erst, nachdem dievalue -Smethod aufgerufen wurde.

Die sfactory-Methodelater erstellt aus einer Funktion eineEval-Instanz. Die Auswertung wird bis zum Aufruf vonvalue verschoben und das Ergebnis gespeichert:

var counter : Int = 0
val later = Eval.later { counter++; counter }
Assert.assertEquals(0, counter)

val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)

val secondValue = later.value()
Assert.assertEquals(1, secondValue)
Assert.assertEquals(1, counter)

Die dritte Fabrik istalways. Es wird eineEval-Instanz erstellt, die die angegebene Funktion jedes Mal neu berechnet, wennvalue aufgerufen wird:

var counter : Int = 0
val later = Eval.always { counter++; counter }
Assert.assertEquals(0, counter)

val firstValue = later.value()
Assert.assertEquals(1, firstValue)
Assert.assertEquals(1, counter)

val secondValue = later.value()
Assert.assertEquals(2, secondValue)
Assert.assertEquals(2, counter)

4. Fehlerbehandlungsmuster mit funktionalen Datentypen

Die Fehlerbehandlung durch Auslösen von Ausnahmen hat mehrere Nachteile.

Bei Methoden, die häufig und vorhersehbar fehlschlagen, z. B. das Parsen von Benutzereingaben als Zahl, ist es kostspielig und unnötig, Ausnahmen auszulösen. Der größte Teil der Kosten stammt aus derfillInStackTrace-Methode. In modernen Frameworks kann die Stack-Ablaufverfolgung mit überraschend wenig Informationen zur Geschäftslogik lächerlich lang werden.

Darüber hinaus kann die Behandlung geprüfter Ausnahmen den Code des Clients leicht unnötig komplizieren. Andererseits hat der Aufrufer bei Laufzeitausnahmen keine Informationen über die Möglichkeit einer Ausnahme.

Als Nächstes implementieren wir eine Lösung, um herauszufinden, ob der größte Teiler der geraden Eingangsnummer eine quadratische Zahl ist. Die Benutzereingabe wird als Zeichenfolge eingehen. Zusammen mit diesem Beispiel untersuchen wir, wie die Datentypen von Arrow bei der Fehlerbehandlung helfen können

4.1. Fehlerbehandlung mitOption

Zuerst analysieren wir die Eingabezeichenfolge als Ganzzahl.

Glücklicherweise hat Kotlin eine praktische, ausnahmesichere Methode:

fun parseInput(s : String) : Option = Option.fromNullable(s.toIntOrNull())

Wir wickeln das Analyseergebnis inOption ein. Anschließend transformieren wir diesen Anfangswert mit einer benutzerdefinierten Logik:

fun isEven(x : Int) : Boolean // ...
fun biggestDivisor(x: Int) : Int // ...
fun isSquareNumber(x : Int) : Boolean // ...

Dank des Designs vonOption wird unsere Geschäftslogik nicht mit Ausnahmebehandlungen und If-else-Zweigen überladen:

fun computeWithOption(input : String) : Option {
    return parseInput(input)
      .filter(::isEven)
      .map(::biggestDivisor)
      .map(::isSquareNumber)
}

Wie wir sehen können, handelt es sich um reinen Geschäftscode ohne die Last technischer Details.

Mal sehen, wie ein Kunde mit dem Ergebnis arbeiten kann:

fun computeWithOptionClient(input : String) : String {
    val computeOption = computeWithOption(input)
    return when(computeOption) {
        is None -> "Not an even number!"
        is Some -> "The greatest divisor is square number: ${computeOption.t}"
    }
}

Das ist großartig, aber der Client hat keine detaillierten Informationen darüber, was mit der Eingabe falsch war.

Schauen wir uns nun an, wie wir eine detailliertere Beschreibung eines Fehlerfalls mitEither liefern können.

4.2 Error Handling with Either

Wir haben verschiedene Möglichkeiten, Informationen über den Fehlerfall mitEither zurückzugeben. Auf der linken Seite könnten wir eine String-Nachricht, einen Fehlercode oder sogar eine Ausnahme einfügen.

Für diesen Zweck erstellen wir zunächst eine versiegelte Klasse:

sealed class ComputeProblem {
    object OddNumber : ComputeProblem()
    object NotANumber : ComputeProblem()
}

Wir nehmen diese Klasse in die zurückgegebenenEither auf. Bei der Analysemethode verwenden wir die Factory-Funktion voncond:

Either.cond( /Condition/, /Right-side provider/, /Left-side provider/)

Anstelle vonOption, wird alsoEither in unsereparseInput-Methode verwenden:

fun parseInput(s : String) : Either =
  Either.cond(s.toIntOrNull() != null, { -> s.toInt() }, { -> ComputeProblem.NotANumber } )

Dies bedeutet, dassEither miteither der Nummer oder des Fehlerobjekts gefüllt werden.

Alle anderen Funktionen sind dieselben wie zuvor. Die Methodefilter unterscheidet sich jedoch fürEither. Es erfordert nicht nur ein Prädikat, sondern auch einen Anbieter auf der linken Seite für den falschen Zweig des Prädikats:

fun computeWithEither(input : String) : Either {
    return parseInput(input)
      .filterOrElse(::isEven) { -> ComputeProblem.OddNumber }
      .map (::biggestDivisor)
      .map (::isSquareNumber)
}

Dies liegt daran, dass wir die andere Seite derEither liefern müssen, falls unser Filterfalse zurückgibt.

Jetzt weiß der Client genau, was mit seiner Eingabe falsch war:

fun computeWithEitherClient(input : String) {
    val computeWithEither = computeWithEither(input)
    when(computeWithEither) {
        is Either.Right -> "The greatest divisor is square number: ${computeWithEither.b}"
        is Either.Left -> when(computeWithEither.a) {
            is ComputeProblem.NotANumber -> "Wrong input! Not a number!"
            is ComputeProblem.OddNumber -> "It is an odd number!"
        }
    }
}

5. Fazit

Die Arrow-Bibliothek wurde erstellt, um Funktionsmerkmale in Kotlin zu unterstützen. Wir haben die bereitgestellten Datentypen im Arrow-Core-Paket untersucht. Dann haben wirOptional undEither für die Fehlerbehandlung im Funktionsstil verwendet.

Wie immer ist der Codeover on GitHub verfügbar.