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!
quelle
Applicative
die Essenz des Iteratormusters ist.Antworten:
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 '
quelle
[]
undMaybe
Instanzen herumzuspielen, bekommt man ein Gefühl dafür, welche FormApplicative
hat 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.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, dassIO
es sich um einen anwendbaren Funktor handelt, zur Rettung:Prelude Control.Applicative> (++) <$> getLine <*> getLine hi there "hithere"
Obwohl Sie ausdrücklich nach Nicht-
Maybe
Beispielen 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ührteNothing
). Im Wesentlichen, weil Sie "Teilwerte" haben, möchten Sie Ihre Funktion in eine Teilfunktion verwandeln, die undefiniert ist, wenn eine ihrer Eingaben undefiniert ist. DannPrelude 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 GrundFunctor
ist, dass es Funktionen beliebiger Aritätfmap
heben kann , während es nur eine unäre Funktion heben kann.quelle
(| (++) getLine getLine |)
der Reihenfolge der beidengetLine
Aktionen wird es für das Ergebnis von Bedeutung ...(<*>)
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) }
Monad
Instanzen(<*>)
=ap
, wodurch die Reihenfolge so festgelegt wird, dass sie meinem obigen Beispiel entspricht.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
do
Notation haben, führt die Verwendung eines anwendbaren Stils häufig zu kompakterem Code.In diesem Beispiel haben wir einen Typ
Foo
und möchten zufällige Werte dieses Typs erstellen. Mit der Monadeninstanz fürIO
könnten wir schreibendata 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
liftM2
eine ä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.
quelle
[Foo x y | x <- randomIO, y <- randomIO]
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
quelle
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 #.
quelle
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 :)
quelle
<$>
ist noch ansprechender, dafmap
es sich standardmäßig nicht um einen Infix-Operator handelt. Also müsste es eher so sein:fmap (toGregorian . utctDay) getCurrentTime
fmap
ist, dass es nicht funktioniert, wenn Sie eine einfache Funktion mehrerer Argumente auf mehrere monadische Werte anwenden möchten. Um dies zu lösen,Applicative
kommt dasBei 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.
quelle
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"
quelle
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.
quelle
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.
quelle
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
apply
nur in Ihr Typklassenmodell übersetzen, z. B.<*>
in Scalaz oder Haskell.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.
quelle