Was ist "Heben" in Haskell?


Antworten:


179

Das Heben ist eher ein Entwurfsmuster als ein mathematisches Konzept (obwohl ich erwarte, dass mich hier jemand widerlegt, indem er zeigt, wie Aufzüge eine Kategorie oder etwas sind).

Normalerweise haben Sie einen Datentyp mit einem Parameter. Etwas wie

data Foo a = Foo { ...stuff here ...}

Angenommen , Sie feststellen , dass viele Anwendungen von Foonumerischen Typen nehmen ( Int, Doubleusw.) und Sie halten, die zu schreiben Code, der diese Zahlen auspackt, ergänzt oder vervielfacht sie, und dann wickelt sie sichern. Sie können dies kurzschließen, indem Sie den Unwrap-and-Wrap-Code einmal schreiben. Diese Funktion wird traditionell als "Aufzug" bezeichnet, da sie folgendermaßen aussieht:

liftFoo2 :: (a -> b -> c) -> Foo a -> Foo b -> Foo c

Mit anderen Worten, Sie haben eine Funktion, die eine Funktion mit zwei Argumenten (z. B. den (+)Operator) in die entsprechende Funktion für Foos umwandelt.

Jetzt können Sie also schreiben

addFoo = liftFoo2 (+)

Bearbeiten: Weitere Informationen

Sie können natürlich haben liftFoo3, liftFoo4und so weiter. Dies ist jedoch häufig nicht erforderlich.

Beginnen Sie mit der Beobachtung

liftFoo1 :: (a -> b) -> Foo a -> Foo b

Aber das ist genau das gleiche wie fmap. Also eher als liftFoo1du schreiben würdest

instance Functor Foo where
   fmap f foo = ...

Wenn Sie wirklich vollständige Regelmäßigkeit wollen, können Sie sagen

liftFoo1 = fmap

Wenn Sie Fooeinen Funktor machen können, können Sie ihn vielleicht zu einem anwendbaren Funktor machen. Wenn Sie schreiben können, liftFoo2sieht die anwendbare Instanz tatsächlich so aus :

import Control.Applicative

instance Applicative Foo where
   pure x = Foo $ ...   -- Wrap 'x' inside a Foo.
   (<*>) = liftFoo2 ($)

Der (<*>)Operator für Foo hat den Typ

(<*>) :: Foo (a -> b) -> Foo a -> Foo b

Es wendet die umschlossene Funktion auf den umschlossenen Wert an. Wenn Sie also implementieren liftFoo2können, können Sie dies in Bezug darauf schreiben. Oder Sie können es direkt implementieren und sich nicht darum kümmern liftFoo2, da das Control.ApplicativeModul enthält

liftA2 :: Applicative f => (a -> b -> c) -> f a -> f b -> f c

und ebenso gibt es liftAund liftA3. Sie werden jedoch nicht sehr oft verwendet, da es einen anderen Operator gibt

(<$>) = fmap

So können Sie schreiben:

result = myFunction <$> arg1 <*> arg2 <*> arg3 <*> arg4

Der Begriff myFunction <$> arg1gibt eine neue Funktion zurück, die in Foo eingeschlossen ist. Dies kann wiederum auf das nächste Argument angewendet werden, indem (<*>)usw. verwendet wird. Anstatt jetzt eine Liftfunktion für jede Arität zu haben, haben Sie nur noch eine Reihe von Anwendungen.


26
Es ist wahrscheinlich erwähnenswert, dass Aufzüge die Standardgesetze einhalten sollten lift id == idund lift (f . g) == (lift f) . (lift g).
Carlos Scheidegger

13
Aufzüge sind in der Tat "eine Kategorie oder so etwas". Carlos hat gerade die Functor-Gesetze aufgelistet, wo idund .sind der Identitätspfeil und die Pfeilzusammensetzung einer Kategorie. Wenn Sie von Haskell sprechen, ist die fragliche Kategorie normalerweise "Hask", dessen Pfeile Haskell-Funktionen sind (mit anderen Worten, idund .beziehen sich auf die Haskell-Funktionen, die Sie kennen und lieben).
Dan Burton

3
Dies sollte lesen instance Functor Foo, nicht instance Foo Functor, nicht wahr? Ich würde mich selbst bearbeiten, bin mir aber nicht 100% sicher.
Amalloy

2
Heben ohne Antrag ist = Functor. Ich meine, Sie haben zwei Möglichkeiten: Functor oder Applicative Functor. Die erste hebt Einzelparameterfunktionen auf, die zweite Mehrparameterfunktion. So ziemlich das war's. Richtig? Es ist keine Raketenwissenschaft :) es klingt einfach so. Danke übrigens für die tolle Antwort!
Jhegedus

2
@atc: Dies ist eine Teilanwendung. Siehe wiki.haskell.org/Partial_application
Paul Johnson

41

Pauls und Yairchus sind beide gute Erklärungen.

Ich möchte hinzufügen, dass die Funktion, die aufgehoben wird, eine beliebige Anzahl von Argumenten haben kann und dass sie nicht vom gleichen Typ sein müssen. Sie können beispielsweise auch einen liftFoo1 definieren:

liftFoo1 :: (a -> b) -> Foo a -> Foo b

Im Allgemeinen wird das Heben von Funktionen, die 1 Argument annehmen, in der Typklasse erfasst Functor, und die Hebeoperation wird aufgerufen fmap:

fmap :: Functor f => (a -> b) -> f a -> f b

Beachten Sie die Ähnlichkeit mit liftFoo1dem Typ. In der Tat können Sie, wenn Sie haben liftFoo1, Fooeine Instanz von erstellen Functor:

instance Functor Foo where
  fmap = liftFoo1

Darüber hinaus wird die Verallgemeinerung des Aufhebens auf eine beliebige Anzahl von Argumenten als Anwendungsstil bezeichnet . Tauchen Sie erst ein, wenn Sie das Aufheben von Funktionen mit einer festen Anzahl von Argumenten verstanden haben. Aber wenn Sie dies tun, hat Learn you a Haskell ein gutes Kapitel dazu. Die Typeclassopedia ist ein weiteres gutes Dokument, das Functor und Applicative beschreibt (sowie andere Typklassen; scrollen Sie zum rechten Kapitel in diesem Dokument).

Hoffe das hilft!


25

Beginnen wir mit einem Beispiel (für eine übersichtlichere Darstellung wird ein Leerraum hinzugefügt):

> import Control.Applicative
> replicate 3 'a'
"aaa"
> :t replicate
replicate        ::         Int -> b -> [b]
> :t liftA2
liftA2 :: (Applicative f) => (a -> b -> c) -> (f a -> f b -> f c)
> :t liftA2 replicate
liftA2 replicate :: (Applicative f) =>       f Int -> f b -> f [b]
> (liftA2 replicate) [1,2,3] ['a','b','c']
["a","b","c","aa","bb","cc","aaa","bbb","ccc"]
> ['a','b','c']
"abc"

liftA2wandelt eine Funktion einfacher Typen in eine Funktion derselben Typen um, die in eine eingeschlossen sindApplicative , z. B. Listen IOusw.

Ein weiterer üblicher Aufzug ist liftvon Control.Monad.Trans. Es wandelt eine monadische Handlung einer Monade in eine Handlung einer transformierten Monade um.

Im Allgemeinen hebt "lift" eine Funktion / Aktion in einen "Wrapped" -Typ um (damit die ursprüngliche Funktion "under the Wraps" funktioniert).

Der beste Weg, dies und Monaden usw. zu verstehen und zu verstehen, warum sie nützlich sind, besteht wahrscheinlich darin, sie zu codieren und zu verwenden. Wenn Sie zuvor etwas codiert haben, von dem Sie vermuten, dass es davon profitieren kann (dh der Code wird dadurch kürzer usw.), probieren Sie es einfach aus und Sie werden das Konzept leicht verstehen.


13

Heben ist ein Konzept, mit dem Sie eine Funktion in eine entsprechende Funktion innerhalb einer anderen (normalerweise allgemeineren) Einstellung umwandeln können

Schauen Sie sich http://haskell.org/haskellwiki/Lifting an


40
Ja, aber diese Seite beginnt mit "Wir beginnen normalerweise mit einem (kovarianten) Funktor ...". Nicht gerade Neuling freundlich.
Paul Johnson

3
Aber "Funktor" ist verlinkt, so dass der Neuling einfach darauf klicken kann, um zu sehen, was ein Funktor ist. Zugegeben, die verlinkte Seite ist nicht so gut. Ich muss ein Konto einrichten und das beheben.
Jrockway

10
Es ist ein Problem, das ich auf anderen funktionalen Programmierseiten gesehen habe. Jedes Konzept wird anhand anderer (unbekannter) Konzepte erklärt, bis sich der Kreis des Neulings schließt (und die Kurve umrundet). Muss etwas damit zu tun haben, Rekursion zu mögen.
DNA

2
Stimmen Sie für diesen Link ab. Lift stellt Verbindungen zwischen einer Welt und einer anderen Welt her.
eccstartup

3
Antworten wie diese sind nur dann gut, wenn Sie das Thema bereits verstanden haben.
DoubleOrt

-2

Nach diesem glänzenden tutorial , ist ein Funktors einige Container (wie Maybe<a>, List<a>oder Tree<a>das kann Speicherelementen irgendeines anderen Typs a). Ich habe die Java-Generika-Notation <a>für den Elementtyp verwendet aund stelle mir die Elemente als Beeren im Baum vor Tree<a>. Es gibt eine Funktion fmap, die eine Elementkonvertierungsfunktion übernimmt, a->bund einen Container functor<a>. Es gilt a->bfür jedes Element des Containers, in das es effektiv umgewandelt wird functor<b>. Wenn nur das erste Argument angegeben wird a->b, fmapwartet auf das functor<a>. Das heißt, a->ballein durch die Lieferung wird diese Funktion auf Elementebene in die Funktion umgewandelt functor<a> -> functor<b>, die über Containern ausgeführt wird. Dies nennt man Hebender Funktion. Da der Container auch als Funktor bezeichnet wird , sind die Funktoren anstelle der Monaden eine Voraussetzung für das Heben. Monaden sind eine Art "parallel" zum Heben. Beide verlassen sich auf den Functor-Begriff und tun dies auch f<a> -> f<b>. Der Unterschied besteht darin, dass das Heben a->bfür die Konvertierung verwendet wird, während Monad vom Benutzer definiert werden muss a -> f<b>.


5
Ich habe dir eine Note gegeben, weil "ein Funktor ist ein Behälter" ein Flammenköder mit Trollgeschmack ist. Beispiel: Funktionen von einigen rbis zu einem Typ (verwenden wir cfür Abwechslung) sind Funktoren. Sie "enthalten" keine c. In diesem Fall ist fmap eine Funktionszusammensetzung, die eine a -> bFunktion und eine r -> aEins übernimmt , um Ihnen eine neue r -> bFunktion zu geben . Immer noch keine Container. Wenn ich könnte, würde ich es auch für den letzten Satz noch einmal notieren.
BMeph

1
Ist auch fmapeine Funktion und "wartet" nicht auf irgendetwas; Der "Container", der ein Funktor ist, ist der springende Punkt beim Heben. Außerdem sind Monaden eher eine doppelte Idee zum Heben: Mit einer Monade können Sie etwas verwenden, das einige Male positiv angehoben wurde, als wäre es nur einmal angehoben worden - dies wird besser als Abflachen bezeichnet .
BMeph

1
@BMeph To wait, to expect, to anticipatesind Synonyme. Mit "Funktion wartet" meinte ich "Funktion antizipiert".
Val

@BMeph Ich würde sagen, anstatt eine Funktion als Gegenbeispiel für die Idee zu betrachten, dass Funktoren Container sind, sollten Sie sich die vernünftige Funktorinstanz von function als Gegenbeispiel für die Idee vorstellen, dass Funktionen keine Container sind. Eine Funktion ist eine Zuordnung von einer Domäne zu einer Codomäne, wobei die Domäne das Kreuzprodukt aller Parameter ist und die Codomäne der Ausgabetyp der Funktion ist. Ebenso ist eine Liste eine Zuordnung der Naturals zum inneren Typ der Liste (Domäne -> Codomäne). Sie werden noch ähnlicher, wenn Sie sich die Funktion merken oder die Liste nicht behalten.
Semikolon

@BMeph Einer der einzigen Gründe, warum Listen eher als Container angesehen werden, ist, dass sie in vielen Sprachen mutiert werden können, während Funktionen dies traditionell nicht können. Aber in Haskell ist selbst das keine faire Aussage, da keine mutiert werden kann und beide kopiermutiert werden können: b = 5 : aund f 0 = 55 f n = g nbeide beinhalten eine Pseudomutation des "Containers". Auch die Tatsache, dass Listen normalerweise vollständig im Speicher gespeichert sind, während Funktionen normalerweise als Berechnung gespeichert werden. Aber Memoizing / Monorphic-Listen, die nicht zwischen Anrufen gespeichert werden, brechen beide den Mist aus dieser Idee heraus.
Semikolon
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.