Qu'est-ce que «lever» dans Haskell?


138

Je ne comprends pas ce qu'est «soulever». Dois-je d'abord comprendre les monades avant de comprendre ce qu'est un «ascenseur»? (Moi aussi, j'ignore complètement les monades :) Ou quelqu'un peut-il me l'expliquer avec des mots simples?


9
Peut-être utile, peut-être pas: haskell.org/haskellwiki/Lifting
kennytm

Réponses:


179

Le levage est plus un modèle de conception qu'un concept mathématique (même si je m'attends à ce que quelqu'un ici me réfute maintenant en montrant en quoi les ascenseurs sont une catégorie ou quelque chose).

En règle générale, vous avez un type de données avec un paramètre. Quelque chose comme

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

Supposons que vous trouviez que de nombreuses utilisations de Footypes numériques prennent ( Int, Doubleetc.) et que vous continuiez à écrire du code qui déballe ces nombres, les ajoute ou les multiplie, puis les rembobine. Vous pouvez court-circuiter cela en écrivant une fois le code de déroulement et d'enroulement. Cette fonction est traditionnellement appelée «ascenseur» car elle ressemble à ceci:

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

En d'autres termes, vous avez une fonction qui prend une fonction à deux arguments (comme l' (+)opérateur) et la transforme en fonction équivalente pour Foos.

Alors maintenant tu peux écrire

addFoo = liftFoo2 (+)

Edit: plus d'informations

Vous pouvez bien sûr avoir liftFoo3, liftFoo4et ainsi de suite. Cependant, cela n'est souvent pas nécessaire.

Commencez par l'observation

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

Mais c'est exactement la même chose que fmap. Alors plutôt que liftFoo1tu écrirais

instance Functor Foo where
   fmap f foo = ...

Si vous voulez vraiment une régularité complète, vous pouvez alors dire

liftFoo1 = fmap

Si vous pouvez en faire Fooun foncteur, vous pouvez peut-être en faire un foncteur applicatif. En fait, si vous pouvez écrire, liftFoo2l'instance applicative ressemble à ceci:

import Control.Applicative

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

L' (<*>)opérateur de Foo a le type

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

Il applique la fonction encapsulée à la valeur encapsulée. Donc, si vous pouvez mettre en œuvre, liftFoo2vous pouvez l'écrire en termes de cela. Ou vous pouvez l'implémenter directement et ne pas vous embêter liftFoo2, car le Control.Applicativemodule comprend

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

et de même il y a liftAet liftA3. Mais vous ne les utilisez pas très souvent car il y a un autre opérateur

(<$>) = fmap

Cela vous permet d'écrire:

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

Le terme myFunction <$> arg1renvoie une nouvelle fonction encapsulée dans Foo. Cela peut à son tour être appliqué à l'argument suivant en utilisant (<*>), et ainsi de suite. Alors maintenant, au lieu d'avoir une fonction d'ascenseur pour chaque arité, vous avez juste une guirlande d'applicatifs.


26
Il vaut probablement la peine de rappeler que les ascenseurs doivent respecter les lois standard lift id == idet lift (f . g) == (lift f) . (lift g).
Carlos Scheidegger

13
Les ascenseurs sont en effet "une catégorie ou quelque chose". Carlos vient d'énumérer les lois Functor, où idet .sont respectivement la flèche d'identité et la composition de flèches d'une certaine catégorie. Habituellement, quand on parle de Haskell, la catégorie en question est "Hask", dont les flèches sont des fonctions Haskell (en d'autres termes, idet .font référence aux fonctions Haskell que vous connaissez et aimez).
Dan Burton

3
Cela devrait lire instance Functor Foo, non instance Foo Functor, non? Je modifierais moi-même mais je ne suis pas sûr à 100%.
amalloy

2
Le levage sans applicatif est = Functor. Je veux dire que vous avez 2 choix: Functor ou Applicative Functor. Le premier soulève les fonctions à paramètre unique les secondes fonctions à paramètres multiples. C'est à peu près tout. Droite? Ce n'est pas sorcier :) ça sonne juste comme ça. Merci pour la bonne réponse btw!
jhegedus

2
@atc: il s'agit d'une application partielle. Voir wiki.haskell.org/Partial_application
Paul Johnson

41

Paul et yairchu sont tous deux de bonnes explications.

Je voudrais ajouter que la fonction levée peut avoir un nombre arbitraire d'arguments et qu'ils ne doivent pas nécessairement être du même type. Par exemple, vous pouvez également définir un liftFoo1:

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

En général, la levée des fonctions qui prennent 1 argument est capturée dans la classe de type Functoret l'opération de levage est appelée fmap:

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

Notez la similitude avec liftFoo1le type de. En fait, si c'est le cas liftFoo1, vous pouvez créer Fooune instance de Functor:

instance Functor Foo where
  fmap = liftFoo1

De plus, la généralisation du levage à un nombre arbitraire d'arguments est appelée style applicatif . Ne vous donnez pas la peine de plonger dans cela jusqu'à ce que vous ayez saisi la levée des fonctions avec un nombre fixe d'arguments. Mais quand vous le faites, Learn you a Haskell a un bon chapitre à ce sujet. Le Typeclassopedia est un autre bon document qui décrit Functor et Applicative (ainsi que d'autres classes de types; faites défiler jusqu'au chapitre droit de ce document).

J'espère que cela t'aides!


25

Commençons par un exemple (un espace blanc est ajouté pour une présentation plus claire):

> 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"

liftA2transforme une fonction de types simples en une fonction des mêmes types enveloppée dans unApplicative , comme des listes IO, etc.

Un autre ascenseur commun est liftde Control.Monad.Trans. Il transforme une action monadique d'une monade en une action d'une monade transformée.

En général, "lever" lève une fonction / action dans un type "enveloppé" (de sorte que la fonction d'origine se met au travail "sous les enveloppes").

La meilleure façon de comprendre cela, et les monades etc., et de comprendre pourquoi elles sont utiles, est probablement de les coder et de les utiliser. S'il y a quelque chose que vous avez précédemment codé et que vous soupçonnez de pouvoir en bénéficier (c'est-à-dire que cela raccourcira le code, etc.), essayez-le et vous comprendrez facilement le concept.


13

Le levage est un concept qui vous permet de transformer une fonction en une fonction correspondante dans un autre paramètre (généralement plus général)

jetez un œil à http://haskell.org/haskellwiki/Lifting


40
Ouais, mais cette page commence "Nous commençons généralement par un foncteur (covariant) ...". Pas vraiment sympa pour les débutants.
Paul Johnson

3
Mais "fonctor" est lié, donc le débutant peut simplement cliquer dessus pour voir ce qu'est un Functor. Certes, la page liée n'est pas si bonne. J'ai besoin d'un compte et de résoudre ce problème.
jrockway

10
C'est un problème que j'ai vu sur d'autres sites de programmation fonctionnelle; chaque concept est expliqué en termes d'autres concepts (inconnus) jusqu'à ce que le débutant fasse un tour complet (et contournant le virage). Doit être quelque chose à voir avec aimer la récursivité.
DNA

2
Votez pour ce lien. Lift établit des connexions entre un monde et un autre monde.
eccstartup

3
Des réponses comme celle-ci ne sont bonnes que lorsque vous comprenez déjà le sujet.
doubleOrt

-2

Selon ce tutoriel brillant , un foncteur est un conteneur (comme Maybe<a>, List<a>ou Tree<a>qui peut stocker des éléments d'un autre type, a). J'ai utilisé la notation générique Java,, <a>pour le type d'élément aet je considère les éléments comme des baies sur l'arbre Tree<a>. Il existe une fonction fmapqui prend une fonction de conversion d'élément a->bet un conteneur functor<a>. Il s'applique a->bà chaque élément du conteneur en le convertissant efficacement en functor<b>. Lorsque seul le premier argument est fourni a->b, fmapattend le functor<a>. Autrement dit, la fourniture a->bseule transforme cette fonction au niveau de l'élément en fonction functor<a> -> functor<b>qui opère sur les conteneurs. C'est ce qu'on appelle le levagede la fonction. Parce que le conteneur est également appelé un foncteur , les Functors plutôt que les Monades sont un prérequis pour le levage. Les monades sont en quelque sorte «parallèles» au levage. Les deux s'appuient sur la notion de Functor et font f<a> -> f<b>. La différence est que le levage utilise a->bpour la conversion alors que Monad oblige l'utilisateur à définir a -> f<b>.


5
Je vous ai donné une note, parce que "un foncteur est un conteneur" est un appât de flamme à saveur de troll. Exemple: les fonctions de certains rà un type (utilisons cpour la variété), sont des Functors. Ils ne «contiennent» aucun c. Dans ce cas, fmap est une composition de fonction, prenant une a -> bfonction et une r -> aautre, pour vous donner une nouvelle r -> bfonction. Toujours pas de conteneurs Et si je pouvais, je le marquerais à nouveau pour la dernière phrase.
BMeph

1
Aussi, fmapest une fonction, et n'attend rien; Le "container" étant un Functor, c'est tout l'intérêt du levage. En outre, les Monades sont, si quelque chose, une double idée de la levée: une Monade vous permet d'utiliser quelque chose qui a été soulevé un certain nombre de fois positif, comme s'il n'avait été soulevé qu'une seule fois - c'est mieux connu sous le nom d' aplatissement .
BMeph

1
@BMeph To wait, to expect, to anticipatesont les synonymes. En disant «fonction attend», je voulais dire «fonction anticipe».
Val

@BMeph Je dirais qu'au lieu de penser à une fonction comme un contre-exemple à l'idée que les foncteurs sont des conteneurs, vous devriez penser à l'instance de foncteur sensée de la fonction comme un contre-exemple à l'idée que les fonctions ne sont pas des conteneurs. Une fonction est un mappage d'un domaine à un codomaine, le domaine étant le produit croisé de tous les paramètres, le codomain étant le type de sortie de la fonction. De la même manière, une liste est une correspondance entre les Naturals et le type interne de la liste (domaine -> codomain). Ils deviennent encore plus similaires si vous mémorisez la fonction ou ne gardez pas la liste.
point

@BMeph l'une des seules raisons pour lesquelles les listes sont davantage considérées comme un conteneur est que dans de nombreuses langues, elles peuvent être mutées, alors que traditionnellement les fonctions ne le peuvent pas. Mais chez Haskell, même ce n'est pas une déclaration juste car aucun des deux ne peut être muté, et les deux peuvent être mutés par copie: b = 5 : aet f 0 = 55 f n = g n, les deux impliquent une pseudo-mutation du "conteneur". Aussi le fait que les listes sont généralement stockées complètement en mémoire alors que les fonctions sont généralement stockées sous forme de calcul. Mais la mémorisation / les listes monorphes qui ne sont pas stockées entre les appels cassent la merde de cette idée.
point
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.