Entschuldigung, ich kenne meine Mathematik nicht wirklich, daher bin ich gespannt, wie die Funktionen in der anwendbaren Typklasse ausgesprochen werden
Ihre Mathematik zu kennen oder nicht, ist hier weitgehend irrelevant, denke ich. Wie Sie wahrscheinlich wissen, leiht sich Haskell ein paar Begriffe aus verschiedenen Bereichen der abstrakten Mathematik, insbesondere der Kategorietheorie , aus denen wir Funktoren und Monaden erhalten. Die Verwendung dieser Begriffe in Haskell weicht etwas von den formalen mathematischen Definitionen ab, aber sie sind normalerweise nahe genug, um ohnehin gute beschreibende Begriffe zu sein.
Die Applicative
Typklasse liegt irgendwo zwischen Functor
und Monad
, daher würde man erwarten, dass sie eine ähnliche mathematische Basis hat. Die Dokumentation zum Control.Applicative
Modul beginnt mit:
Dieses Modul beschreibt eine Struktur zwischen einem Funktor und einer Monade: Es bietet reine Ausdrücke und Sequenzierung, aber keine Bindung. (Technisch gesehen ein starker laxer monoidaler Funktor.)
Hmm.
class (Functor f) => StrongLaxMonoidalFunctor f where
. . .
Nicht ganz so eingängig wie Monad
, denke ich.
Im Grunde Applicative
läuft alles darauf hinaus, dass es keinem mathematisch besonders interessanten Konzept entspricht. Es liegen also keine vorgefertigten Begriffe herum, die die Art und Weise erfassen, wie es in Haskell verwendet wird. Legen Sie also die Mathematik vorerst beiseite.
Wenn wir wissen wollen, wie wir es nennen sollen, kann (<*>)
es hilfreich sein zu wissen, was es im Grunde bedeutet.
Also, was ist überhaupt los Applicative
und warum nennen wir es so?
Was Applicative
in der Praxis bedeutet, ist eine Möglichkeit, beliebige Funktionen in a zu heben Functor
. Betrachten Sie die Kombination von Maybe
(wohl der einfachste nicht triviale Functor
) und Bool
(ebenfalls der einfachste nicht triviale Datentyp).
maybeNot :: Maybe Bool -> Maybe Bool
maybeNot = fmap not
Mit dieser Funktion fmap
können wir not
von der Arbeit Bool
zur Arbeit übergehen Maybe Bool
. Aber was ist, wenn wir heben wollen (&&)
?
maybeAnd' :: Maybe Bool -> Maybe (Bool -> Bool)
maybeAnd' = fmap (&&)
Nun, das wollen wir überhaupt nicht ! In der Tat ist es so ziemlich nutzlos. Wir können versuchen , schlau zu sein und heimlich eine andere Bool
in Maybe
durch die Hintertür ...
maybeAnd'' :: Maybe Bool -> Bool -> Maybe Bool
maybeAnd'' x y = fmap ($ y) (fmap (&&) x)
... aber das ist nicht gut. Zum einen ist es falsch. Zum anderen ist es hässlich . Wir könnten es weiter versuchen, aber es stellt sich heraus, dass es keine Möglichkeit gibt, eine Funktion mehrerer Argumente aufzuheben, um an einer beliebigen zu arbeitenFunctor
. Nervig!
Auf der anderen Seite konnten wir es einfach tun , wenn wir gebraucht Maybe
‚s Monad
Beispiel:
maybeAnd :: Maybe Bool -> Maybe Bool -> Maybe Bool
maybeAnd x y = do x' <- x
y' <- y
return (x' && y')
Das ist eine Menge Aufwand, nur um eine einfache Funktion zu übersetzen - weshalb Control.Monad
eine Funktion bereitgestellt wird, um dies automatisch zu tun liftM2
. Die 2 in ihrem Namen bezieht sich auf die Tatsache, dass sie mit Funktionen von genau zwei Argumenten arbeitet; Ähnliche Funktionen gibt es für 3, 4 und 5 Argumentfunktionen. Diese Funktionen sind besser , aber nicht perfekt, und die Angabe der Anzahl der Argumente ist hässlich und ungeschickt.
Das bringt uns zu dem Artikel, in dem die Typklasse "Anwendbar" eingeführt wurde . Darin machen die Autoren im Wesentlichen zwei Beobachtungen:
- Das Aufheben von Funktionen mit mehreren Argumenten in eine
Functor
ist eine sehr natürliche Sache
- Dies erfordert nicht die volle Leistungsfähigkeit von a
Monad
Die Anwendung mit normalen Funktionen wird durch einfaches Nebeneinander von Begriffen geschrieben. Um die "angehobene Anwendung" so einfach und natürlich wie möglich zu gestalten, werden Infix-Operatoren vorgestellt, die für die Anwendung stehen, in dieFunctor
und eine Typklasse gehoben werden , um das bereitzustellen, was dafür benötigt wird .
All dies bringt uns zu folgendem Punkt: (<*>)
Stellt einfach die Funktionsanwendung dar - warum sollte man sie also anders aussprechen als den Leerzeichen-Operator "Nebeneinander"?
Aber wenn das nicht sehr befriedigend ist, können wir beobachten, dass das Control.Monad
Modul auch eine Funktion bietet, die dasselbe für Monaden tut:
ap :: (Monad m) => m (a -> b) -> m a -> m b
Wo ap
ist natürlich die Abkürzung für "bewerben". Da jedes sein Monad
kann Applicative
und ap
nur die Teilmenge der in letzterem vorhandenen Merkmale benötigt, können wir vielleicht sagen, dass es aufgerufen werden sollte , wenn (<*>)
es kein Operator wäre ap
.
Wir können uns den Dingen auch aus der anderen Richtung nähern. Der Functor
Hebevorgang wird aufgerufen, fmap
weil er eine Verallgemeinerung des map
Vorgangs auf Listen darstellt. Welche Art von Funktion auf Listen würde funktionieren (<*>)
? Da ist wasap
gibt natürlich das, auf Listen steht, aber das allein ist nicht besonders nützlich.
In der Tat gibt es eine vielleicht natürlichere Interpretation für Listen. Was fällt Ihnen ein, wenn Sie sich die folgende Typensignatur ansehen?
listApply :: [a -> b] -> [a] -> [b]
Die Idee, die Listen parallel aufzustellen und jede Funktion in der ersten auf das entsprechende Element der zweiten anzuwenden, ist einfach so verlockend. Unglücklicherweise für unseren alten Freund verstößtMonad
diese einfache Operation gegen die Monadengesetze, wenn die Listen unterschiedlich lang sind. Aber es macht eine Geldstrafe Applicative
, in welchem Fall (<*>)
wird eine Möglichkeit, eine verallgemeinerte Version von aneinander zu reihen zipWith
, also können wir uns vielleicht vorstellen, es zu nennen fzipWith
?
Diese Zipping-Idee schließt den Kreis. Erinnern Sie sich an das Mathe-Zeug früher über monoidale Funktoren? Wie der Name schon sagt, können Sie auf diese Weise die Struktur von Monoiden und Funktoren kombinieren, die beide bekannte Haskell-Typklassen sind:
class Functor f where
fmap :: (a -> b) -> f a -> f b
class Monoid a where
mempty :: a
mappend :: a -> a -> a
Wie würden diese aussehen, wenn Sie sie in eine Schachtel legen und ein wenig aufrütteln würden? Von Functor
behalten wir die Idee einer Struktur unabhängig von ihrem Typparameter und von Monoid
behalten wir die Gesamtform der Funktionen bei:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ?
mfAppend :: f ? -> f ? -> f ?
Wir wollen nicht davon ausgehen, dass es eine Möglichkeit gibt, ein wirklich "leeres" zu erstellen Functor
, und wir können keinen Wert eines beliebigen Typs heraufbeschwören, also werden wir den Typ von mfEmpty
as festlegen f ()
.
Wir möchten auch nicht erzwingen mfAppend
, dass ein konsistenter Typparameter benötigt wird. Jetzt haben wir Folgendes:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f ?
Wofür ist der Ergebnistyp mfAppend
? Wir haben zwei beliebige Typen, von denen wir nichts wissen, daher haben wir nicht viele Optionen. Am sinnvollsten ist es, einfach beides zu behalten:
class (Functor f) => MonoidalFunctor f where
mfEmpty :: f ()
mfAppend :: f a -> f b -> f (a, b)
An diesem Punkt mfAppend
ist jetzt eindeutig eine verallgemeinerte Version von zip
On-Listen, und wir können Applicative
leicht rekonstruieren :
mfPure x = fmap (\() -> x) mfEmpty
mfApply f x = fmap (\(f, x) -> f x) (mfAppend f x)
Dies zeigt uns auch, dass dies pure
mit dem Identitätselement von a zusammenhängt Monoid
, sodass andere gute Namen dafür alles sein können, was auf einen Einheitswert, eine Nulloperation oder dergleichen hindeutet.
Das war langwierig, um es zusammenzufassen:
(<*>)
ist nur eine modifizierte Funktionsanwendung, sodass Sie sie entweder als "ap" oder "apply" lesen oder ganz wie bei einer normalen Funktionsanwendung entfernen können.
(<*>)
Verallgemeinert auch grob zipWith
auf Listen, so dass Sie es als "Zip-Funktoren mit" lesen können, ähnlich wie fmap
als "Karte eines Funktors mit".
Die erste ist näher an der Absicht der Applicative
Typklasse - wie der Name schon sagt - und das empfehle ich.
Tatsächlich ermutige ich die liberale Verwendung und Nichtaussprache aller aufgehobenen Anwendungsbetreiber :
(<$>)
, wodurch eine Einzelargumentfunktion in eine Functor
(<*>)
, die eine Multi-Argument-Funktion durch eine verkettet Applicative
(=<<)
, die eine Funktion bindet, die a in eine Monad
vorhandene Berechnung eingibt
Alle drei sind im Kern nur reguläre Funktionsanwendungen, die ein wenig aufgewertet wurden.