51 Stimmen

Funktionssyntax-Rätsel in scalaz

Nach der Beobachtung Nick Partidges Präsentation bei der Ableitung scalaz Ich habe mir dieses Beispiel angeschaut, das einfach großartig ist:

import scalaz._
import Scalaz._
def even(x: Int) : Validation[NonEmptyList[String], Int] 
    = if (x % 2 ==0) x.success else "not even: %d".format(x).wrapNel.fail

println( even(3) <|*|> even(5) ) //prints: Failure(NonEmptyList(not even: 3, not even: 5))

Ich habe versucht zu verstehen, was die <|*|> Methode war, hier ist der Quellcode:

def <|*|>[B](b: M[B])(implicit t: Functor[M], a: Apply[M]): M[(A, B)] 
    = <**>(b, (_: A, _: B))

OK, das ist ziemlich verwirrend (!) - aber es verweist auf die <**> Methode, die so deklariert ist:

def <**>[B, C](b: M[B], z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C] 
    = a(t.fmap(value, z.curried), b)

Ich habe also ein paar Fragen:

  1. Wie kommt es, dass die Methode anscheinend eine übergeordneter Typ eines Typparameters ( M[B] ), kann aber durch eine Validation (die zwei Typen von Paremetern hat)?
  2. Die Syntax (_: A, _: B) definiert die Funktion (A, B) => Pair[A,B] was die 2. Methode erwartet: Was geschieht mit dem Tupel2/Paar im Falle eines Fehlers? Es ist kein Tupel in Sicht!

10 Stimmen

O_O Das ist der Grund, warum selbst in der funktionalen Programmierung beschreibende Namen von Funktionen eine gute Praxis sind.

4 Stimmen

Und welchen beschreibenden Namen würden Sie dafür vorschlagen? Ich denke, dass ein symbolischer Name viel besser ist, vorausgesetzt, er ist in der gesamten Literatur (z. B. FP-Papiere) konsistent.

13 Stimmen

Beschreibende "Namen" sind enthalten. Sie werden "Typen" genannt. Das Beste an diesen Namen ist, dass sie robuster sind, maschinell geprüft werden und mehr Informationen liefern.

64voto

retronym Punkte 54220

Typkonstrukteure als Typparameter

M ist ein Typ-Parameter für einen der wichtigsten Zuhälter von Scalaz, MA der den Type Constructor (auch bekannt als Higher Kinded Type) des aufgemotzten Wertes darstellt. Dieser Typkonstruktor wird verwendet, um die entsprechenden Instanzen von Functor y Apply , die implizite Anforderungen an die Methode sind <**> .

trait MA[M[_], A] {
   val value: M[A]
   def <**>[B, C](b: M[B], z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C] = ...
}

Was ist ein Typ-Konstruktor?

Aus der Scala-Sprachreferenz:

Wir unterscheiden zwischen erster Ordnung Typen und Typkonstruktoren, die Typparameter annehmen und Typen liefern. Eine Untermenge der Typen erster Ordnung, genannt Werttypen repräsentiert Mengen von (erstklassigen) Werten. Wertetypen sind entweder konkret oder abstrakt. Jeder konkreter Wertetyp kann dargestellt werden als Klassentyp, d.h. als Typ Bezeichner (§3.2.3), der sich auf eine Klasse1 (§5.3) verweist, oder als zusammengesetzter Typ (§3.2.7), der eine Schnittmenge von Typen, möglicherweise mit einer Verfeinerung (§3.2.7), die die Typen ihrer Mitglieder weiter einschränkt. Typen seinerMitglieder weiter einschränkt. Abstrakter Wert Typen werden eingeführt durch Typ Parameter (§4.4) und abstrakte Typ Bindungen (§4.3). Klammern in Typen werden zur Gruppierung verwendet. Wir gehen davon aus, dass Objekte und Pakete auch implizit eine Klasse definieren (mit dem gleichen Namen wie das Objekt oder Paket, aber unzugänglich für Benutzerprogramme).

Nichtwerttypen erfassen Eigenschaften von Identifikatoren, die keine Werte sind (§3.3). Zum Beispiel, ein Typ Konstruktor (§3.3.3) nicht direkt den Typ von Werten an. Jedoch, wenn ein Typkonstruktor angewendet wird auf die richtigen Typargumente angewendet wird, ergibt er einen Typ erster Ordnung, bei dem es sich um einen Werttyp sein kann. Nicht-Wert-Typen werden in Scala indirekt ausgedrückt. Zum Beispiel wird ein Methodentyp wird beschrieben, indem man eine Methodensignatur aufgeschrieben, die an sich selbst kein echter Typ ist, obwohl sie zu einem entsprechenden Funktionstyp Typ (§3.3.1). Typkonstruktoren sind ein weiteres Beispiel, denn man kann den Typ Swap[m[_, _], a,b] = m[b, a], aber es gibt keine Syntax zum Schreiben der entsprechende anonyme Typfunktion direkt zu schreiben.

List ist ein Typkonstruktor. Sie können den Typ Int um einen Werttyp zu erhalten, List[Int] die einen Wert klassifizieren kann. Andere Typkonstruktoren benötigen mehr als einen Parameter.

Der Charakterzug scalaz.MA erfordert, dass der erste Typparameter ein Typkonstruktor sein muss, der einen einzelnen Typ annimmt und einen Werttyp zurückgibt, mit der Syntax trait MA[M[_], A] {} . Die Definition des Typparameters beschreibt die Form des Typkonstruktors, die als Kind bezeichnet wird. List soll die Art * -> * .

Teilweise Anwendung von Typen

Aber wie kann MA einen Wert des Typs Validation[X, Y] ? Der Typ Validation hat eine Art (* *) -> * und kann nur als Typargument an einen Typparameter übergeben werden, der wie folgt deklariert ist M[_, _] .

Diese implizite Umwandlung in Objekt Scalaz konvertiert einen Wert des Typs Validation[X, Y] zu einer MA :

object Scalaz {
    implicit def ValidationMA[A, E](v: Validation[E, A]): MA[PartialApply1Of2[Validation, E]#Apply, A] = ma[PartialApply1Of2[Validation, E]#Apply, A](v)
}

Das wiederum verwendet einen Trick mit einem Typ-Alias in TeilweiseAnwendung1von2 um den Typkonstruktor teilweise anzuwenden Validation und fixiert die Art der Fehler, lässt aber die Art des Erfolgs unangewendet.

PartialApply1Of2[Validation, E]#Apply würde besser geschrieben werden als [X] => Validation[E, X] . Ich habe vor kurzem vorgeschlagen, eine solche Syntax zu Scala hinzufügen, könnte es in 2.9 passieren.

Betrachten Sie dies als ein Äquivalent auf der Ebene des Typs hiervon:

def validation[A, B](a: A, b: B) = ...
def partialApply1Of2[A, B C](f: (A, B) => C, a: A): (B => C) = (b: B) => f(a, b)

Damit können Sie Folgendes kombinieren Validation[String, Int] mit einer Validation[String, Boolean] weil sie beide den Typkonstruktor [A] Validation[String, A] .

Anwendbare Funktoren

<**> fordert den Typkonstruktor M müssen assoziierte Instanzen von Bewerbung y Faktor . Dies stellt einen Applicative Functor dar, der, wie eine Monade, eine Möglichkeit ist, eine Berechnung durch einen Effekt zu strukturieren. In diesem Fall besteht der Effekt darin, dass die Unterberechnungen fehlschlagen können (und wenn sie das tun, werden die Fehlschläge akkumuliert).

Der Container Validation[NonEmptyList[String], A] kann einen reinen Wert des Typs A bei dieser "Wirkung". Die <**> Operator nimmt zwei wirksame Werte und eine reine Funktion und kombiniert sie mit der Applicative Functor-Instanz für diesen Container.

So funktioniert es für die Option applikativer Funktor. Die "Wirkung" ist hier die Möglichkeit des Scheiterns.

val os: Option[String] = Some("a")
val oi: Option[Int] = Some(2)

val result1 = (os <**> oi) { (s: String, i: Int) => s * i }
assert(result1 == Some("aa"))

val result2 = (os <**> (None: Option[Int])) { (s: String, i: Int) => s * i }
assert(result2 == None)

In beiden Fällen gibt es eine reine Funktion vom Typ (String, Int) => String die auf wirksame Argumente angewandt werden. Beachten Sie, dass das Ergebnis in denselben Effekt (oder Container, wenn Sie so wollen) verpackt ist wie die Argumente.

Sie können dasselbe Muster für eine Vielzahl von Containern verwenden, die einen zugehörigen Applicative Functor haben. Alle Monaden sind automatisch Applicative Functoren, aber es gibt noch mehr, wie ZipStream .

Option y [A]Validation[X, A] sind beides Monaden, man könnte also auch Bind (auch bekannt als flatMap):

val result3 = oi flatMap { i => os map { s => s * i } }
val result4 = for {i <- oi; s <- os} yield s * i

Tupling mit `<|**|>`

<|**|> ist wirklich ähnlich wie <**> aber es stellt die reine Funktion zur Verfügung, mit der Sie einfach ein Tuple2 aus den Ergebnissen erstellen können. (_: A, _ B) ist eine Abkürzung für (a: A, b: B) => Tuple2(a, b)

Und darüber hinaus

Hier sind unsere gebündelten Beispiele für Anwendbar y Validierung . Ich habe eine etwas andere Syntax verwendet, um den Applicative Functor zu verwenden, (fa fb fc fd) {(a, b, c, d) => .... }

UPDATE: Aber was passiert im Fall des Scheiterns?

was mit dem Tupel2/Paar im Falle eines Fehlers geschieht ?

Wenn eine der Unterberechnungen fehlschlägt, wird die angegebene Funktion nie ausgeführt. Sie wird nur ausgeführt, wenn alle Teilberechnungen (in diesem Fall die beiden Argumente, die an <**> ) erfolgreich sind. Ist dies der Fall, werden diese zu einem Success . Wo ist diese Logik? Dies definiert die Apply Instanz für [A] Validation[X, A] . Wir verlangen, dass der Typ X eine Semigroup zur Verfügung, d. h. die Strategie zur Kombination der einzelnen Fehler, jeweils vom Typ X in einen aggregierten Fehler der gleichen Art. Wenn Sie wählen String als Fehlertyp angeben, wird die Semigroup[String] verkettet die Zeichenketten; wenn Sie NonEmptyList[String] werden die Fehler aus den einzelnen Schritten zu einem längeren Ergebnis verkettet NonEmptyList von Fehlern. Diese Verkettung geschieht unten, wenn zwei Failures kombiniert werden, indem die Operator (der sich mit Implikaten erweitert, z. B. zu, Scalaz.IdentityTo(e1).(e2)(Semigroup.NonEmptyListSemigroup(Semigroup.StringSemigroup)) .

implicit def ValidationApply[X: Semigroup]: Apply[PartialApply1Of2[Validation, X]#Apply] = new Apply[PartialApply1Of2[Validation, X]#Apply] {
  def apply[A, B](f: Validation[X, A => B], a: Validation[X, A]) = (f, a) match {
    case (Success(f), Success(a)) => success(f(a))
    case (Success(_), Failure(e)) => failure(e)
    case (Failure(e), Success(_)) => failure(e)
    case (Failure(e1), Failure(e2)) => failure(e1  e2)
  }
}

Monade oder Applicative, wie soll ich mich entscheiden?

Lesen Sie noch? ( Ja. Ed )

Ich habe gezeigt, dass Teilberechnungen auf der Grundlage von Option o [A] Validation[E, A] kann entweder kombiniert werden mit Apply oder mit Bind . Wann würden Sie das eine dem anderen vorziehen?

Wenn Sie Apply ist die Struktur der Berechnung festgelegt. Alle Teilberechnungen werden ausgeführt; die Ergebnisse der einen können die anderen nicht beeinflussen. Nur die 'reine' Funktion hat einen Überblick über das, was passiert ist. Bei monadischen Berechnungen hingegen kann die erste Teilberechnung die späteren beeinflussen.

Wenn wir eine monadische Validierungsstruktur verwenden würden, würde der erste Fehler die gesamte Validierung zum Erliegen bringen, da es keine Success Wert, der in die anschließende Validierung einfließt. Wir sind jedoch froh, dass die Teilvalidierungen unabhängig sind, so dass wir sie durch den Applicative kombinieren und alle Fehler, auf die wir stoßen, sammeln können. Die Schwäche der Applicative Functors hat sich in eine Stärke verwandelt!

1 Stimmen

Ich hätte das Wort "Monade" nicht verwenden sollen: Entschuldigung. Mir ist klar, dass M[_] beschreibt eine übergeordneter Typ . Ich habe die Frage jetzt bearbeitet

5 Stimmen

Wirklich schöne und ausführliche Antwort.

0 Stimmen

Ich habe die Antwort entsprechend bearbeitet, um mich auf Typkonstruktoren und applikative Funktoren zu konzentrieren, anstatt auf das Fehlen von Monaden :)

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X