Unwissenschaftlich über Monaden

Hallo an alle.

Nach vier Jahren Programmierung auf Scala ist mein Verständnis von Monaden endlich so weit gewachsen, dass man es anderen erklären kann, ohne auf die Kategorietheorie und die klassische Monade Bezug zu nehmen - es ist nur ein Monoid in der Kategorie der Endofunder , das Programmierer nicht schlechter macht als Dichlorvos-Kakerlaken.

Codebeispiele werden in Kotlin, as geschrieben es ist sehr beliebt und gleichzeitig sehr funktional (in beiden Sinne des Wortes).

Beginnen wir mit dem Konzept eines Funktors , hier ist es:

interface Functor<A> 

Was bedeutet es? Ein Funktor ist eine Abstraktion einer beliebigen Berechnung, die ein Ergebnis vom Typ A zurückgibt. Wir abstrahieren davon, wie ein neuer Funktor erstellt wird und vor allem, wie sein Wert A berechnet wird. Insbesondere kann sich eine Funktion hinter einer Funktorschnittstelle verstecken mit einer beliebigen Anzahl von Argumenten und nicht unbedingt einer reinen Funktion.

Beispiele für Funktorimplementierungen:

  • konstant
  • Funktion mit einer beliebigen Anzahl von Argumenten, die ein Ergebnis vom Typ A zurückgibt
  • Zustand Pseudozufallsgenerator (Random)
  • Hardware-Zufallszahlengenerator
  • Lesen eines Objekts von der Festplatte oder vom Netzwerk
  • asynchrone Berechnung - Ein Rückruf wird an die Funktorimplementierung übergeben, die später aufgerufen wird

Alle diese Beispiele mit Ausnahme der Konstanten haben eine wichtige Eigenschaft - sie sind faul, d.h. Die Berechnung selbst erfolgt nicht beim Erstellen des Funktors, sondern beim Berechnen.

Die Funktorschnittstelle ermöglicht weder das Abrufen eines Werts vom Typ A von Funktor Functor<A> noch das Erstellen eines neuen Functor<A> aus einem vorhandenen Wert vom Typ A Aber selbst mit solchen Einschränkungen ist der Funktor nicht nutzlos - wenn wir für einen Typ B A in B konvertieren können (mit anderen Worten, es gibt eine Funktion (a: A) -> B ), dann können wir eine Funktion schreiben (f: Functor<A>) -> Functor<B> und nennen Sie es map :

 interface Functor<A> { fun <B> map(f: (A) -> B): Functor<B> } 

Im Gegensatz zum Funktor selbst kann die Kartenmethode keine beliebige Funktion sein:
- map((a) -> a) sollte denselben Funktor zurückgeben
- map((a) -> f(a)).map((b) -> g(b)) muss mit map(a -> g(f(a)) identisch sein.

Als Beispiel implementieren wir einen Funktor, der einen A-Wert zurückgibt, der eine bestimmte Anzahl von Zufallsbits enthält. Unsere Schnittstelle in Kotlin kann nicht so einfach verwendet werden (aber Sie können es , falls gewünscht), daher werden wir eine Erweiterungsmethode schreiben:

 //  - ,     ,   map data class MyRandom<A>( val get: (bits: Int) -> A ) { companion object { val intRandom: MyRandom<Int> = MyRandom { Random.nextBits(it) } val hexRandom: MyRandom<String> = intRandom.map { it.toString(16) } } } //  map   fun <A, B> MyRandom<A>.map(f: (A) -> B): MyRandom<B> = MyRandom(get = {bits -> f(get(bits)) }) fun main(args: Array<String>) { println("random=" + MyRandom.intRandom.get(12)) //  random=1247 println("hexRandom=" + MyRandom.hexRandom.get(12)) //  hexRandom=c25 } 

Andere Beispiele für Funktoren mit einer nützlichen map

  • unveränderliche List<A>
  • MyInputStream<A>
  • Optional<A>

Jetzt können Sie zu Monaden gehen.

Eine Monade ist ein Funktor mit zwei zusätzlichen Operationen. Erstens enthält die Monade im Gegensatz zum Funktor die Operation des Erzeugens aus einer Konstanten. Diese Operation wird als lift :

 fun <A> lift(value: A): Monad<A> = TODO() 

Die zweite Operation heißt flatMap und ist komplizierter. Zuerst geben wir unsere gesamte Monadenschnittstelle an:

 interface Monad<A> { //   ,  map     - //    flatMap  lift fun <B> map(f: (A) -> B): Monad<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Monad<B>): Monad<B> } fun <A> lift(value: A): Monad<A> = TODO() 

Der wichtigste Unterschied zwischen einer Monade und einem Funktor besteht darin, dass Monaden miteinander kombiniert werden können, um neue Monaden zu erzeugen und von der Implementierung der Monade zu abstrahieren - ob sie von der Festplatte liest, ob sie zusätzliche Parameter zur Berechnung ihres Werts akzeptiert, existiert dieser Wert . Der zweite wichtige Punkt - Monaden werden nicht parallel, sondern nacheinander kombiniert, sodass je nach Ergebnis der ersten Monade Logik hinzugefügt werden kann.

Ein Beispiel:

 // ,     Int //       -      //           val readInt: Monad<Int> = TODO() // ,      -  fun readBytes(len: Int): Monad<ByteArray> = TODO() // ,     ,    val bytes: Monad<ByteArray> = readInt.flatMap {len -> if (len > 0) readBytes(len) //    -   else lift(ByteArray(0)) //  ,    } 

In diesem Beispiel wird jedoch kein Netzwerk erwähnt. Ebenso gut können Daten aus einer Datei oder aus einer Datenbank gelesen werden. Sie können synchron oder asynchron gelesen werden, hier kann es zu einer Fehlerbehandlung kommen - alles hängt von der spezifischen Implementierung der Monade ab, der Code selbst bleibt unverändert.

Das Beispiel ist zunächst einfacher, Option Monade. In Kotlin wird es nicht wirklich benötigt, aber in Java / Scala ist es äußerst nützlich:

 data class Option<A>(val value: A?) { fun <B> map(f: (A) -> B): Option<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> Option<B>): Option<B> = when(value) { null -> Option(null) else -> f(value) } } fun <A> lift(value: A?): Option<A> = Option(value) fun mul(a: Option<Int>, b: Option<Int>): Option<Int> = a.flatMap { a -> b.map { b -> a * b } } fun main(args: Array<String>) { println(mul(Option(4), Option(5)).value) // 20 println(mul(Option(null), Option(5)).value) // null println(mul(Option(4), Option(null)).value) // null println(mul(Option(null), Option(null)).value) // null } 

Lassen Sie uns als Monade der Pozakovyristie die Arbeit mit der Datenbank in der Monade abschließen:

 data class DB<A>(val f: (Connection) -> A) { fun <B> map(f: (A) -> B): DB<B> = flatMap { a -> lift(f(a)) } fun <B> flatMap(f: (A) -> DB<B>): DB<B> = DB { conn -> f(this.f(conn)).f(conn) } } fun <A> lift(value: A): DB<A> = DB { value } fun select(id: Int): DB<String> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun update(value: String): DB<Unit> = DB { conn -> val st = conn.createStatement() // .... TODO() } fun selectThenUpdate(id: Int): DB<Unit> = select(id).flatMap { value -> update(value) } fun executeTransaction(c: Connection): Unit { //  ,     //          val body: DB<Unit> = selectThenUpdate(42) //  ,   select  update body.f(c) c.commit() } 

Ist das Kaninchenloch tief?


Es gibt eine Vielzahl von Monaden, aber ihr Hauptzweck besteht darin, die Geschäftslogik der Anwendung von einigen Details der durchgeführten Berechnungen zu abstrahieren:

  • dass der Wert möglicherweise nicht vorhanden ist: data class Option<A>(value: A?)
  • dass die Berechnung fehlschlägt: data class Either<Error, A>(value: Pair<Error?, A?>)
  • dass die Berechnung faul sein kann: data class Defer<A>(value: () -> A)
  • oder asynchron: java.util.concurrent.CompletableFuture<A>
  • oder einen Funktionszustand haben: data class State<S, A>(value: (S) -> Pair<S, A>)

Liste der unbeantworteten Fragen:

  • Applikative Funktoren - eine Zwischenverbindung zwischen Funktoren und Monaden
  • Sammlungen wie Monaden
  • Kompositionen monotypischer Monaden - Pfeilkleber, monadische Transformatoren
  • Sequenz / Traverse
  • Monaden als Effekte
  • Monaden und Rekursion, Stapelüberlauf, Trampolin
  • Tagless endgültige Codierung
  • Io Monade
  • und im Allgemeinen der ganze Zoo der Standardmonaden

Was weiter?


Pfeil-kt.io
typelevel.org/cats/typeclasses.html
wiki.haskell.org/All_About_Monads

Mein Experiment ist eine vollwertige FP-Anwendung auf Scala:
github.com/scf37/fpscala2

PS Ich wollte eine kleine Notiz, es stellte sich wie immer heraus.

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


All Articles