Was sind praktische Anwendungen des Applikationsstils?

71

Ich bin ein Scala-Programmierer und lerne jetzt Haskell. Es ist einfach, praktische Anwendungsfälle und Beispiele aus der Praxis für OO-Konzepte wie Dekorateure, Strategiemuster usw. zu finden. Bücher und Interwebs sind damit gefüllt.

Mir wurde klar, dass dies bei funktionalen Konzepten irgendwie nicht der Fall ist. Ein typisches Beispiel: Antragsteller .

Ich habe Probleme, praktische Anwendungsfälle für Antragsteller zu finden. Fast alle Tutorials und Bücher, auf die ich bisher gestoßen bin, enthalten Beispiele für []und Maybe. Ich habe erwartet, dass Applikative anwendbarer sind, da ich die ganze Aufmerksamkeit sehe, die sie in der FP-Community erhalten.

Ich glaube, ich verstehe die konzeptionelle Grundlage für Anwendungen (vielleicht irre ich mich) und habe lange auf meinen Moment der Erleuchtung gewartet. Aber es scheint nicht zu passieren. Nie während des Programmierens hatte ich einen Moment, in dem ich vor Freude schrie: "Eureka! Ich kann hier Anwendung anwenden!" (außer wieder für []und Maybe).

Kann mir bitte jemand zeigen, wie Applikative in einer täglichen Programmierung verwendet werden können? Wie fange ich an, das Muster zu erkennen? Vielen Dank!

fehlender Faktor
quelle
1
Zum ersten Mal wurde ich durch diese beiden Artikel dazu inspiriert, dieses Zeug zu lernen: debasishg.blogspot.com/2010/11/exploring-scalaz.html debasishg.blogspot.com/2011/02/…
CheatEx
eng verwandt: stackoverflow.com/questions/2120509/…
Mauricio Scheffer
In dem Artikel Die Essenz des Iteratormusters geht es darum, wie Applicativedie Essenz des Iteratormusters ist.
Russell O'Connor

Antworten:

11

Warnung: Meine Antwort ist eher predigend / entschuldigend. So verklage mich.

Wie oft erstellen Sie in Ihrer täglichen Haskell-Programmierung neue Datentypen? Klingt so, als ob Sie wissen möchten, wann Sie Ihre eigene Applicative-Instanz erstellen müssen, und ehrlich gesagt müssen Sie wahrscheinlich nicht viel tun, es sei denn, Sie rollen Ihren eigenen Parser. Verwenden von Anwendungsinstanzen verwenden, sollten Sie lernen, dies häufig zu tun.

Anwendbar ist kein "Designmuster" wie Dekorateure oder Strategien. Es ist eine Abstraktion, die es viel durchdringender und allgemein nützlicher macht, aber viel weniger greifbar. Der Grund, warum Sie Schwierigkeiten haben, "praktische Verwendungen" zu finden, liegt darin, dass die Beispielverwendungen dafür fast zu einfach sind. Sie verwenden Dekoratoren, um Fenster mit Bildlaufleisten zu versehen. Sie verwenden Strategien, um die Benutzeroberfläche für aggressive und defensive Bewegungen Ihres Schachbots zu vereinheitlichen. Aber wofür sind Anwendungen? Nun, sie sind viel allgemeiner, daher ist es schwer zu sagen, wofür sie sind, und das ist in Ordnung. Applikatoren sind praktisch als Analysekombinatoren; Das Yesod-Webframework verwendet Applicative, um Informationen aus Formularen einzurichten und zu extrahieren. Wenn Sie schauen, finden Sie eine Million und eine Verwendung für Applicative; es ist überall. Aber da ist es '

Dan Burton
quelle
19
Ich bin überrascht, dass diese Antwort mit einem Häkchen versehen wurde, während einige andere Antworten wie Hammar und Oliver weit unten auf der Seite stehen. Ich schlage vor, dass diese überlegen sind, weil sie hervorragende Beispiele für Anwendungen außerhalb von Maybe und [] bieten. Dem Fragesteller zu sagen, er solle etwas tiefer denken, ist einfach nicht hilfreich.
Darrint
1
@darrint - anscheinend fand der Fragesteller es hilfreich, da er derjenige ist, der es als akzeptiert markiert hat. Ich stehe zu dem, was ich gesagt habe: Wenn man Zeit damit verbringt, selbst mit Gerechten []und MaybeInstanzen herumzuspielen, bekommt man ein Gefühl dafür, welche Form Applicativehat und wie sie verwendet wird. Dies macht jede Typklasse nützlich: Sie muss nicht unbedingt genau wissen, was jede Instanz tut, sondern eine allgemeine Vorstellung davon haben, was anwendbare Kombinatoren im Allgemeinen tun. Wenn Sie also auf einen neuen Datentyp stoßen und erfahren, dass er eine anwendbare Instanz hat können Sie es sofort verwenden.
Dan Burton
72

Applikative sind großartig, wenn Sie eine einfache alte Funktion mehrerer Variablen haben und die Argumente haben, diese aber in einen bestimmten Kontext eingebunden sind. Sie haben beispielsweise die einfache alte Verkettungsfunktion (++), möchten sie jedoch auf zwei Zeichenfolgen anwenden, die über E / A erfasst wurden. Dann kommt die Tatsache, dass IOes sich um einen anwendbaren Funktor handelt, zur Rettung:

Prelude Control.Applicative> (++) <$> getLine <*> getLine
hi
there
"hithere"

Obwohl Sie ausdrücklich nach Nicht- MaybeBeispielen gefragt haben , scheint es mir ein großartiger Anwendungsfall zu sein, deshalb werde ich ein Beispiel geben. Sie haben eine reguläre Funktion mehrerer Variablen, wissen jedoch nicht, ob Sie alle benötigten Werte haben (einige von ihnen konnten möglicherweise nicht berechnet werden, was zu einer Ausbeute führte Nothing). Im Wesentlichen, weil Sie "Teilwerte" haben, möchten Sie Ihre Funktion in eine Teilfunktion verwandeln, die undefiniert ist, wenn eine ihrer Eingaben undefiniert ist. Dann

Prelude Control.Applicative> (+) <$> Just 3 <*> Just 5
Just 8

aber

Prelude Control.Applicative> (+) <$> Just 3 <*> Nothing
Nothing

Welches ist genau das, was Sie wollen.

Die Grundidee ist, dass Sie eine reguläre Funktion in einen Kontext "heben", in dem sie auf so viele Argumente angewendet werden kann, wie Sie möchten. Die zusätzliche Kraft von Applicativeüber nur einem Grund Functorist, dass es Funktionen beliebiger Arität fmapheben kann , während es nur eine unäre Funktion heben kann.

Tom Crockett
quelle
2
Ich bin mir nicht sicher, ob das Anwendungsbeispiel für E / A gut ist, da das Anwendungsbeispiel nicht so sehr um die Reihenfolge imho besorgt ist, aber bei (| (++) getLine getLine |)der Reihenfolge der beiden getLineAktionen wird es für das Ergebnis von Bedeutung ...
hvr
2
@hvr: In welcher Reihenfolge die (<*>)Dinge sortiert werden, ist willkürlich, wird aber normalerweise von Konvention von links nach rechts festgelegt, so dassf <$> x <*> y ==do { x' <- x; y' <- y; return (f x y) }
CA McCann
2
@hvr: Nun, denken Sie daran, dass der Ausdruck selbst nicht von der Sequenzierung abhängen kann , da die angehobene Funktion den Unterschied nicht beobachten kann und beide Effekte auftreten, egal was passiert . Welche Reihenfolge gewählt wird, wird allein von der Instanz definiert, die wissen sollte, welche richtig ist. Beachten Sie außerdem, dass in der Dokumentation angegeben ist, dass für MonadInstanzen (<*>)=ap , wodurch die Reihenfolge so festgelegt wird, dass sie meinem obigen Beispiel entspricht.
CA McCann
1
Die Operatoren <$> und <*> werden als "infixl 4" deklariert, sodass es keine mehrdeutige Konvention gibt. Sie wird mit der Deklaration angegeben, dass sie von links nach rechts gruppiert / zugeordnet werden. Die Reihenfolge der r2l- oder l2r-Effekte wird weiterhin von der tatsächlichen Instanz gesteuert, die für Monaden dieselbe Reihenfolge wie "Control.Monad.ap" verwendet, dh "liftM2 id", und es ist dokumentiert, dass liftM2 von links nach rechts ausgeführt wird.
Chris Kuklewicz
1
@ Chris, die Gruppierung von links nach rechts hat jedoch nichts mit der Ausführung von links nach rechts zu tun.
Rotsor
51

Da viele Applikative auch Monaden sind, hat diese Frage meiner Meinung nach zwei Seiten.

Warum sollte ich die Anwendungsschnittstelle anstelle der monadischen verwenden, wenn beide verfügbar sind?

Dies ist meistens eine Frage des Stils. Obwohl Monaden den syntaktischen Zucker der doNotation haben, führt die Verwendung eines anwendbaren Stils häufig zu kompakterem Code.

In diesem Beispiel haben wir einen Typ Foound möchten zufällige Werte dieses Typs erstellen. Mit der Monadeninstanz für IOkönnten wir schreiben

data Foo = Foo Int Double

randomFoo = do
    x <- randomIO
    y <- randomIO
    return $ Foo x y

Die Anwendungsvariante ist etwas kürzer.

randomFoo = Foo <$> randomIO <*> randomIO

Natürlich könnten wir liftM2eine ähnliche Kürze erzielen, aber der Anwendungsstil ist ordentlicher, als sich auf aritätsspezifische Hebefunktionen verlassen zu müssen.

In der Praxis verwende ich Applikative meistens ähnlich wie den punktfreien Stil: Um zu vermeiden, dass Zwischenwerte benannt werden, wenn eine Operation klarer als Zusammensetzung anderer Operationen ausgedrückt wird.

Warum sollte ich ein Applikativ verwenden wollen, das keine Monade ist?

Da Applikative eingeschränkter sind als Monaden, können Sie nützlichere statische Informationen über sie extrahieren.

Ein Beispiel hierfür sind anwendbare Parser. Während monadische Parser die sequentielle Komposition unterstützen (>>=) :: Monad m => m a -> (a -> m b) -> m b, verwenden nur anwendbare Parser (<*>) :: Applicative f => f (a -> b) -> f a -> f b. Die Typen machen den Unterschied deutlich: Bei monadischen Parsern kann sich die Grammatik je nach Eingabe ändern, während bei einem anwendbaren Parser die Grammatik festgelegt ist.

Indem wir die Schnittstelle auf diese Weise einschränken, können wir beispielsweise bestimmen, ob ein Parser die leere Zeichenfolge akzeptiert, ohne sie auszuführen . Wir können auch die ersten und folgenden Sätze bestimmen, die zur Optimierung verwendet werden können, oder, wie ich kürzlich gespielt habe, Parser erstellen, die eine bessere Fehlerbehebung unterstützen.

Hammar
quelle
4
iinm, die kürzlich neu hinzugefügten Monadenverständnisse in ghc bieten fast das gleiche Maß an Kompaktheit wie anwendbare Kombinatoren:[Foo x y | x <- randomIO, y <- randomIO]
Dan Burton
3
@ Dan: Das ist sicherlich kürzer als das 'do'-Beispiel, aber es ist immer noch nicht punktfrei, was in der Haskell-Welt wünschenswert zu sein scheint
Jared Updike
16

Ich denke an Functor, Applicative und Monad als Designmuster.

Stellen Sie sich vor, Sie möchten eine Future [T] -Klasse schreiben. Das heißt, eine Klasse, die Werte enthält, die berechnet werden sollen.

In einer Java-Denkweise können Sie es wie folgt erstellen

trait Future[T] {
  def get: T
}

Wobei 'get' blockiert, bis der Wert verfügbar ist.

Sie können dies erkennen und neu schreiben, um einen Rückruf zu erhalten:

trait Future[T] {
  def foreach(f: T => Unit): Unit
}

Aber was passiert dann, wenn es zwei Verwendungszwecke für die Zukunft gibt? Dies bedeutet, dass Sie eine Liste der Rückrufe führen müssen. Was passiert auch, wenn eine Methode ein Future [Int] erhält und eine Berechnung basierend auf dem Int im Inneren zurückgeben muss? Oder was tun Sie, wenn Sie zwei Futures haben und etwas basierend auf den von ihnen bereitgestellten Werten berechnen müssen?

Wenn Sie jedoch mit FP-Konzepten vertraut sind, wissen Sie, dass Sie die Future-Instanz manipulieren können, anstatt direkt an T zu arbeiten.

trait Future[T] {
  def map[U](f: T => U): Future[U]
}

Jetzt ändert sich Ihre Anwendung so, dass Sie jedes Mal, wenn Sie an dem enthaltenen Wert arbeiten müssen, eine neue Zukunft zurückgeben.

Sobald Sie auf diesem Weg beginnen, können Sie dort nicht mehr aufhören. Sie erkennen, dass Sie zur Manipulation von zwei Futures nur als Anwendung modellieren müssen, um Futures zu erstellen, eine Monadendefinition für die Zukunft usw. benötigen.

UPDATE: Wie von @Eric vorgeschlagen, habe ich einen Blog-Beitrag geschrieben: http://www.tikalk.com/incubator/blog/functional-programming-scala-rest-us

IttayD
quelle
1
Dies ist eine interessante Möglichkeit, Functor, Applicatives und Monads vorzustellen. Es lohnt sich, einen vollständigen Blog-Beitrag zu verfassen, in dem die Details hinter 'etc ...' aufgeführt sind.
Eric
Link scheint ab heute unterbrochen zu sein. Der Link zur Wayback-Maschine lautet web.archive.org/web/20140604075710/http://www.tikalk.com/…
Superjos
14

Endlich habe ich verstanden, wie Applikative bei dieser Präsentation bei der täglichen Programmierung helfen können:

https://web.archive.org/web/20100818221025/http://applicative-errors-scala.googlecode.com/svn/artifacts/0.6/chunk-html/index.html

Der Autor zeigt, wie Applikative helfen können, Validierungen zu kombinieren und Fehler zu behandeln.

Die Präsentation ist in Scala, aber der Autor bietet auch das vollständige Codebeispiel für Haskell, Java und C #.

paradigmatisch
quelle
2
Der Link ist leider defekt.
thSoft
1
Wayback-Maschinenlink: web.archive.org/web/20100818221025/http://…
Superjos
9

Ich denke, Applicatives erleichtern die allgemeine Verwendung von monadischem Code. Wie oft hatten Sie die Situation, dass Sie eine Funktion anwenden wollten, die Funktion jedoch nicht monadisch war und der Wert, auf den Sie sie anwenden möchten, monadisch ist? Für mich: ziemlich oft!
Hier ist ein Beispiel, das ich gestern geschrieben habe:

ghci> import Data.Time.Clock
ghci> import Data.Time.Calendar
ghci> getCurrentTime >>= return . toGregorian . utctDay

im Vergleich dazu mit Applicative:

ghci> import Control.Applicative
ghci> toGregorian . utctDay <$> getCurrentTime

Diese Form sieht "natürlicher" aus (zumindest für meine Augen :)

oliver
quelle
2
Eigentlich ist <$> nur fmap, es wird aus Data.Functor erneut exportiert.
Sjoerd Visscher
1
@Sjoerd Visscher: richtig ... Die Verwendung von <$>ist noch ansprechender, da fmapes sich standardmäßig nicht um einen Infix-Operator handelt. Also müsste es eher so sein:fmap (toGregorian . utctDay) getCurrentTime
Oliver
1
Das Problem dabei fmapist, dass es nicht funktioniert, wenn Sie eine einfache Funktion mehrerer Argumente auf mehrere monadische Werte anwenden möchten. Um dies zu lösen, Applicativekommt das
CA McCann
2
@oliver Ich denke, was Sjoerd gesagt hat, ist, dass das, was Sie zeigen, nicht wirklich ein Beispiel dafür ist, wo Anwendungen nützlich sind, da Sie wirklich nur mit einem Funktor zu tun haben. Es spielt präsentieren , wie applicative Stil nützlich allerdings ist.
kqr
7

Bei Applicative von "Functor" wird "fmap" verallgemeinert, um das Ausdrücken mehrerer Argumente (liftA2) oder einer Folge von Argumenten (unter Verwendung von <*>) einfach auszudrücken.

Bei Applicative von "Monad" wird die Berechnung nicht von dem berechneten Wert abhängen. Insbesondere können Sie keine Musterübereinstimmung und Verzweigung für einen zurückgegebenen Wert durchführen. In der Regel können Sie ihn nur an einen anderen Konstruktor oder eine andere Funktion übergeben.

Daher sehe ich Applicative als zwischen Functor und Monad eingeklemmt. Das Erkennen, wenn Sie nicht auf die Werte einer monadischen Berechnung verzweigen, ist eine Möglichkeit, um festzustellen, wann Sie zu "Anwendbar" wechseln müssen.

Chris Kuklewicz
quelle
5

Hier ist ein Beispiel aus dem Aeson-Paket:

data Coord = Coord { x :: Double, y :: Double }

instance FromJSON Coord where
   parseJSON (Object v) = 
      Coord <$>
        v .: "x" <*>
        v .: "y"
Qubital
quelle
4

Es gibt einige ADTs wie ZipList, die anwendbare Instanzen haben können, aber keine monadischen Instanzen. Dies war ein sehr hilfreiches Beispiel für mich, als ich den Unterschied zwischen Applikativen und Monaden verstand. Da so viele Anwendungen auch Monaden sind, ist es leicht, den Unterschied zwischen den beiden ohne ein konkretes Beispiel wie ZipList nicht zu erkennen.

Sukant Hajra
quelle
2

Ich denke, es könnte sich lohnen, die Quellen von Paketen auf Hackage zu durchsuchen und aus erster Hand zu sehen, wie anwendbare Funktoren und dergleichen in vorhandenem Haskell-Code verwendet werden.

Artyom Shalkhakov
quelle
2
Es lohnt sich, hier einen bestimmten Link oder weitere Details hinzuzufügen.
Vlad Patryshev
1

Ich habe in einer Diskussion, die ich unten zitiere, ein Beispiel für die praktische Verwendung des Anwendungsfunktors beschrieben.

Beachten Sie, dass die Codebeispiele Pseudocode für meine hypothetische Sprache sind, der die Typklassen in einer konzeptionellen Form der Untertypisierung verbirgt. Wenn Sie also einen Methodenaufruf sehen, müssen Sie applynur in Ihr Typklassenmodell übersetzen, z. B. <*>in Scalaz oder Haskell.

Wenn wir Elemente eines Arrays oder einer Hashmap mit nulloder nonemarkieren, um anzuzeigen, dass ihr Index oder Schlüssel gültig und dennoch wertlos ist, werden die Applicative Werte aktiviert, ohne dass eine Kesselplatte die wertlosen Elemente überspringt, während Operationen auf die Elemente angewendet werden, die einen Wert haben. Und was noch wichtiger ist, es kann automatisch jede WrappedSemantik verarbeiten, die a priori unbekannt ist, dh Operationen Tüber Hashmap[Wrapped[T]](jede über jede Ebene der Komposition, zHashmap[Wrapped[Wrapped2[T]]] weil das Anwendbare ist, die Monade jedoch nicht).

Ich kann mir bereits vorstellen, wie mein Code dadurch leichter verständlich wird. Ich kann mich auf die Semantik konzentrieren, nicht auf die ganze Cruft, um mich dorthin zu bringen, und meine Semantik wird unter der Erweiterung von Wrapped geöffnet, während Ihr gesamter Beispielcode nicht geöffnet ist.

Bezeichnenderweise habe ich vergessen , bevor darauf hinweisen, dass Ihre vorherigen Beispiele nicht den Rückgabewert der emulieren Applicative, die eine sein wird List, nicht ein Nullable, Optionoder Maybe. Selbst meine Versuche, Ihre Beispiele zu reparieren, wurden nicht nachgeahmt Applicative.apply.

Denken Sie daran, dass dies die functionToApplyEingabe für ist Applicative.apply, damit der Container die Kontrolle behält.

list1.apply( list2.apply( ... listN.apply( List.lift(functionToApply) ) ... ) )

Gleichwertig.

list1.apply( list2.apply( ... listN.map(functionToApply) ... ) )

Und mein vorgeschlagener syntaktischer Zucker, den der Compiler in das Obige übersetzen würde.

funcToApply(list1, list2, ... list N)

Es ist nützlich, diese interaktive Diskussion zu lesen , da ich hier nicht alles kopieren kann. Ich erwarte, dass diese URL nicht kaputt geht, wenn man bedenkt, wer der Besitzer dieses Blogs ist. Zum Beispiel zitiere ich weiter unten in der Diskussion.

Die Verschmelzung des Kontrollflusses außerhalb der Anweisung mit der Zuweisung wird von den meisten Programmierern wahrscheinlich nicht gewünscht

Applicative.apply dient zur Verallgemeinerung der teilweisen Anwendung von Funktionen auf parametrisierte Typen (auch als Generika bezeichnet) auf jeder Ebene der Verschachtelung (Zusammensetzung) des Typparameters. Hier geht es darum, eine allgemeinere Komposition zu ermöglichen. Die Allgemeinheit kann nicht erreicht werden, indem sie außerhalb der abgeschlossenen Bewertung (dh des Rückgabewerts) der Funktion gezogen wird, analog zu der Zwiebel, die nicht von innen nach außen geschält werden kann.

Es handelt sich also nicht um eine Verschmelzung, sondern um einen neuen Freiheitsgrad, der Ihnen derzeit nicht zur Verfügung steht. Laut unserem Diskussionsthread müssen Sie deshalb Ausnahmen auslösen oder in einer globalen Variablen speichern, da Ihre Sprache diesen Freiheitsgrad nicht hat. Und das ist nicht die einzige Anwendung dieser Kategorietheorie-Funktoren (in meinem Kommentar in der Moderator-Warteschlange erläutert).

Ich habe einen Link zu einem Beispiel für die Zusammenfassung der Validierung in Scala, F # und C # bereitgestellt , das derzeit in der Moderatorwarteschlange steckt. Vergleichen Sie die abscheuliche C # -Version des Codes. Und der Grund ist, dass das C # nicht verallgemeinert ist. Ich erwarte intuitiv, dass die fallspezifische C # -Kesselplatte geometrisch explodiert, wenn das Programm wächst.

Shelby Moore III
quelle