¿Qué es "levantar" en Haskell?

138

No entiendo qué es "levantar". ¿Debería entender primero las mónadas antes de entender qué es un "ascensor"? (También soy completamente ignorante acerca de las mónadas :) ¿O alguien puede explicármelo con palabras simples?

GabiMe
fuente
9
Quizás útil, quizás no: haskell.org/haskellwiki/Lifting
kennytm

Respuestas:

179

Levantar es más un patrón de diseño que un concepto matemático (aunque espero que alguien por aquí ahora me refute al mostrarme cómo los elevadores son una categoría o algo así).

Por lo general, tiene algún tipo de datos con un parámetro. Algo como

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

Suponga que encuentra muchos usos de Foolos tipos numéricos de toma ( Int, Doubleetc.) y sigue teniendo que escribir código que desenvuelva estos números, los sume o los multiplique y luego los envuelva de nuevo. Puede cortocircuitar esto escribiendo el código de desenvolver y envolver una vez. Esta función se llama tradicionalmente "elevación" porque se ve así:

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

En otras palabras, tiene una función que toma una función de dos argumentos (como el (+)operador) y la convierte en la función equivalente para Foos.

Entonces ahora puedes escribir

addFoo = liftFoo2 (+)

Editar: más información

Por supuesto que puede liftFoo3, liftFoo4y así sucesivamente. Sin embargo, esto a menudo no es necesario.

Comienza con la observación

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

Pero eso es exactamente lo mismo que fmap. Entonces, en lugar de liftFoo1escribir

instance Functor Foo where
   fmap f foo = ...

Si realmente quieres una regularidad completa, puedes decir

liftFoo1 = fmap

Si puedes convertirte Fooen un functor, quizás puedas convertirlo en un functor aplicativo. De hecho, si puede escribir liftFoo2, la instancia aplicativa se ve así:

import Control.Applicative

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

El (<*>)operador de Foo tiene el tipo

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

Aplica la función envuelta al valor envuelto. Entonces, si puede implementar liftFoo2, puede escribir esto en términos de ello. O puede implementarlo directamente y no molestarse liftFoo2, porque el Control.Applicativemódulo incluye

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

e igualmente hay liftAy liftA3. Pero en realidad no los usa muy a menudo porque hay otro operador

(<$>) = fmap

Esto te permite escribir:

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

El término myFunction <$> arg1devuelve una nueva función envuelta en Foo. Esto a su vez se puede aplicar al siguiente argumento usando (<*>), y así sucesivamente. Entonces, en lugar de tener una función de elevación para cada arity, solo tiene una cadena de candidatos.

Paul Johnson
fuente
26
Probablemente valga la pena recordar que los ascensores deben respetar las leyes estándar lift id == idy lift (f . g) == (lift f) . (lift g).
Carlos Scheidegger
13
Los ascensores son de hecho "una categoría o algo". Carlos acaba de enumerar las leyes de Functor, donde idy .son la identidad de la flecha y la composición de la flecha de alguna categoría, respectivamente. Por lo general, cuando se habla de Haskell, la categoría en cuestión es "Hask", cuyas flechas son funciones de Haskell (en otras palabras, idy se .refieren a las funciones de Haskell que conoce y ama).
Dan Burton el
3
Esto debería leer instance Functor Foo, no instance Foo Functor, ¿verdad? Me editaría a mí mismo pero no estoy 100% seguro.
amalloy
2
Levantar sin un Aplicativo es = Functor. Quiero decir que tienes 2 opciones: Functor o Funcional aplicativo. El primer levanta funciones de un solo parámetro, las segundas funciones de múltiples parámetros. Más o menos eso es todo. ¿Correcto? No es ciencia espacial :) simplemente suena así. Gracias por la gran respuesta por cierto!
jhegedus
2
@atc: esta es una aplicación parcial. Ver wiki.haskell.org/Partial_application
Paul Johnson
41

Paul's y yairchu's son buenas explicaciones.

Me gustaría agregar que la función que se levanta puede tener un número arbitrario de argumentos y que no tienen que ser del mismo tipo. Por ejemplo, también podría definir un liftFoo1:

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

En general, el levantamiento de funciones que toman 1 argumento se captura en la clase de tipo Functor, y la operación de levantamiento se llama fmap:

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

Tenga en cuenta la similitud con liftFoo1el tipo de. De hecho, si tiene liftFoo1, puede hacer Foouna instancia de Functor:

instance Functor Foo where
  fmap = liftFoo1

Además, la generalización de levantar a un número arbitrario de argumentos se llama estilo aplicativo . No se moleste en sumergirse en esto hasta que comprenda el levantamiento de funciones con un número fijo de argumentos. Pero cuando lo haces, Learn you a Haskell tiene un buen capítulo sobre esto. El Typeclassopedia es otro buen documento que describe Functor y Aplicativo (así como otras clases de tipos; de desplazamiento hacia abajo a la derecha capítulo en ese documento).

¡Espero que esto ayude!

Martijn
fuente
25

Comencemos con un ejemplo (se agrega algo de espacio en blanco para una presentación más clara):

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

liftA2transforma una función de tipos simples en una función de los mismos tipos envueltos en unApplicative , como listas IO, etc.

Otro ascensor común es liftde Control.Monad.Trans. Transforma una acción monádica de una mónada en una acción de una mónada transformada.

En general, "levantar" eleva una función / acción a un tipo "envuelto" (por lo que la función original funciona "en secreto").

La mejor manera de entender esto, y mónadas, etc., y comprender por qué son útiles, es probablemente codificarlo y usarlo. Si hay algo que codificó anteriormente que sospecha que puede beneficiarse de esto (es decir, esto hará que ese código sea más corto, etc.), simplemente pruébelo y comprenderá fácilmente el concepto.

yairchu
fuente
13

Lifting es un concepto que le permite transformar una función en una función correspondiente dentro de otra configuración (generalmente más general)

Echa un vistazo a http://haskell.org/haskellwiki/Lifting

Nasser Hadjloo
fuente
40
Sí, pero esa página comienza "Usualmente comenzamos con un functor (covariante) ...". No es exactamente novato amigable.
Paul Johnson
3
Pero "functor" está vinculado, por lo que el novato puede hacer clic para ver qué es un Functor. Es cierto que la página vinculada no es tan buena. Necesito obtener una cuenta y arreglar eso.
jrockway
10
Es un problema que he visto en otros sitios de programación funcional; cada concepto se explica en términos de otros conceptos (desconocidos) hasta que el novato completa el círculo (y dobla la curva). Debe tener algo que ver con el gusto por la recursividad.
ADN
2
Vota por este enlace. Lift hace conexiones entre un mundo y otro mundo.
eccstartup
3
Las respuestas como esta solo son buenas cuando ya comprende el tema.
doubleOrt
-2

De acuerdo con este tutorial brillante , un functor es un contenedor (como Maybe<a>, List<a>o Tree<a>que puede almacenar elementos de algún otro tipo a). He usado la notación genérica de Java <a>para el tipo de elemento ay pienso en los elementos como bayas en el árbol Tree<a>. Hay una función fmap, que toma una función de conversión de elementos a->by un contenedor functor<a>. Se aplica a->ba cada elemento del contenedor convirtiéndolo efectivamente en functor<b>. Cuando solo se proporciona el primer argumento a->b, fmapespera el functor<a>. Es decir, el suministro a->bsolo convierte esta función de nivel de elemento en la función functor<a> -> functor<b>que opera sobre contenedores. Esto se llama levantarde la función. Debido a que el contenedor también se llama functor , los Functors en lugar de las Mónadas son un requisito previo para el levantamiento. Las mónadas son algo "paralelas" a la elevación. Ambos confían en la noción de Functor y lo hacen f<a> -> f<b>. La diferencia es que el levantamiento utiliza a->bpara la conversión, mientras que Monad requiere que el usuario lo defina a -> f<b>.

Val
fuente
55
Le di una marca, porque "un functor es un contenedor" es un cebo de llama con sabor a troll. Ejemplo: las funciones de algunos ra un tipo (usemos cpara variedad), son Functores. No "contienen" ninguno c. En este caso, fmap es la composición de funciones, tomando una a -> bfunción y una r -> a, para darle una nueva r -> bfunción. Todavía no hay contenedores. Además, si pudiera, lo marcaría nuevamente para la oración final.
BMeph
1
Además, fmapes una función y no "espera" nada; El "contenedor" siendo un Functor es el punto principal de elevación. Además, las mónadas son, en todo caso, una doble idea para levantar: una mónada le permite usar algo que se ha levantado un número positivo de veces, como si solo se hubiera levantado una vez; esto se conoce mejor como aplanamiento .
BMeph
1
@BMeph To wait, to expect, to anticipateson los sinónimos. Al decir "la función espera" quise decir "la función anticipa".
Val
@BMeph Yo diría que, en lugar de pensar en una función como un contraejemplo de la idea de que los functores son contenedores, debería pensar en la instancia del functor sano de la función como un contraejemplo de la idea de que las funciones no son contenedores. Una función es un mapeo de un dominio a un codominio, siendo el dominio el producto cruzado de todos los parámetros, siendo el codominio el tipo de salida de la función. Del mismo modo, una lista es un mapeo de los naturales al tipo interno de la lista (dominio -> codominio). Se vuelven aún más similares si memoriza la función o no mantiene la lista.
punto
@BMeph, una de las únicas razones por las que se considera más como un contenedor es porque en muchos idiomas pueden mutarse, mientras que tradicionalmente las funciones no pueden. Pero en Haskell, incluso eso no es una afirmación justa, ya que ninguno de los dos puede ser mutado, y ambos pueden ser mutados por copia: b = 5 : ay f 0 = 55 f n = g n, ambos implican seudo mutar el "contenedor". También el hecho de que las listas generalmente se almacenan completamente en la memoria, mientras que las funciones se almacenan típicamente como un cálculo. Pero las listas memorables / monorfas que no se almacenan entre llamadas rompen esa idea.
punto