Die Reader-Monade, oft geschrieben Reader[A, B]
, ist nur der Funktionstyp A => B
. Eine Codierung von Monaden in Scala wie folgt aussieht:
trait Monad[M[_]] {
def pure[A](a: A): M[A]
def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
}
wo map
können in Bezug auf pure
und flatMap
wie so umgesetzt werden:
def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(a => pure(f(a)))
Also erste, was wir tun müssen, ist unsere Binärart zu machen Konstruktor Reader
passt in den unären Typkonstruktor, den Monad
erwartet. Dies geschieht, indem der erste (Eingabe-) Typ-Parameter festgelegt und der zweite (Ausgabe-) Typ-Parameter freigelassen wird.
implicit def readerMonad[X]: Monad[X => ?] = ???
(Die Verwendung von ?
hier ist über die wunderbare kind-projector Compiler Plugin).
Beginnend mit pure
ersetzen wir das Auftreten von M[_]
durch X => _
.
def pure[A](a: A): X => A
einen Wert vom Typ Gegeben A
, müssen wir eine Funktion zurück, die einen Wert vom Typ X
gegeben, gibt einen Wert vom Typ A
. Das einzige, was dies möglicherweise sein kann, ist die konstante Funktion.
def pure[A](a: A): X => A = _ => a
Jetzt flatMap
..
def flatMap[A, B](ma: X => A)(f: A => (X => B)): X => B = (x: X) => ???
Dies ist ein bisschen schwierig ersetzen, aber wir können Arten nutzen, um uns auf die Umsetzung zu führen! Wir haben:
ma: X => A
f: A => (X => B)
x: X
Und wir wollen eine B
bekommen. Die einzige Möglichkeit, das zu tun, ist über f
, die eine A
und eine X
will. Wir haben genau eine X
von x
, aber wir brauchen eine A
. Wir sehen, wir können nur eine A
von ma
bekommen, die eine X
, die uns nur x
bietet uns wünscht. Deshalb haben wir ..
def flatMap[A, B](ma: X => A)(f: A => (X => B)): X => B =
(x: X) => {
val a = ma(x)
val b = f(a)(x)
b
}
laut lesen, flatMap
auf Reader
sagt einen Wert A
aus der "Umwelt" (oder "config") X
zu lesen.Verzweigen Sie dann auf A
, lesen Sie einen anderen Wert B
aus der Umgebung.
Wir gehen weiter und implementieren map
auch manuell:
def map[A, B](ma: X => A)(f: A => B): X => B = ???
an den Argumenten der Suche X => A
und A => B
, mit dem erwarteten Ausgang X => B
, das genau wie Funktion Zusammensetzung aussieht, die in der Tat ist es.
Beispiel der Verwendung cats mit Importen mit:
import cats.data.Reader
Angenommen, wir haben einige Config
Typ:
case class Config(inDev: Boolean, devValue: Int, liveValue: Int)
, die uns sagt, ob wir in einem „dev“ Umwelt sind und gibt wir Werte für etwas für "dev" und für "live". Wir können anfangen, indem wir eine einfache Reader[Config, Boolean]
schreiben, die die inDev
Flagge für uns vorliest.
Und wir können eine Plain-Funktion schreiben, die bei einigen Booleschen Werten den entsprechenden Wert anzeigt.
def branch(flag: Boolean): Reader[Config, Int] =
if (flag) Reader((c: Config) => c.devValue)
else Reader((c: Config) => c.liveValue)
Jetzt können wir bilden die beiden:
val getValue: Reader[Config, Int] =
for {
flag <- inDev
value <- branch(flag)
} yield value
Und jetzt können wir unsere Int
erhalten, indem in verschiedenen Config
Werte übergeben:
getValue.run(Config(true, 1, 2)) // 1
getValue.run(Config(false, 1, 2)) // 2
Dies kann im Allgemeinen als eine verwendet werden netter Weg, um Abhängigkeitsinjektion zu erreichen, mit nur Funktionen!
Thx für die Antwort. Sehr tief. Ich muss das mehrmals lesen und sehen, ob ich es vollständig verstehe. Können Sie mir auch ein paar einfache Anwendungsbeispiele geben? –
Ich habe meine Antwort so bearbeitet, dass sie ein Beispiel enthält :-) – adelbertc