¿Cómo funcionan los functors en haskell?


Estoy tratando de aprender Haskell y estoy a través de todos los conceptos básicos. Pero ahora estoy atascado, tratando de conseguir mi cabeza alrededor de los funtores.

He leído que "Un funtor transforma una categoría en otra categoría". ¿Qué significa esto?

Sé que es mucho pedir, pero ¿podría alguien darme una explicación en inglés simple de los funtores o tal vez un caso de uso simple?

Author: Sarah, 2012-10-30

5 answers

Una explicación borrosa sería que un Functor es algún tipo de contenedor y una función asociada fmap que le permite alterar lo que está contenido, dada una función que transforma lo contenido.

Por ejemplo, las listas son este tipo de contenedor, de modo que fmap (+1) [1,2,3,4] produce [2,3,4,5].

Maybe también se puede hacer un funtor, tal que fmap toUpper (Just 'a') produce Just 'A'.

El tipo general de fmap muestra muy claramente lo que está pasando:

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

Y las versiones especializadas puede hacerlo más claro. Aquí está la versión de la lista:

fmap :: (a -> b) -> [a] -> [b]

Y la versión Maybe:

fmap :: (a -> b) -> Maybe a -> Maybe b

Puede obtener información sobre las instancias estándar Functor consultando GHCI con :i Functor y muchos módulos definen más instancias de Functor s (y otras clases de tipo.)

Por favor, no tome la palabra "contenedor" demasiado en serio, sin embargo. Functor s son un concepto bien definido, pero a menudo se puede razonar al respecto con esta analogía difusa.

Su mejor apuesta para entender lo que está pasando on es simplemente leer la definición de cada una de las instancias, lo que debería darte una intuición sobre lo que está pasando. A partir de ahí es solo un pequeño paso para formalizar realmente su comprensión del concepto. Lo que hay que añadir es una aclaración de lo que realmente es nuestro "contenedor", y que cada instancia satisfaga un par de leyes simples.

 53
Author: Sarah,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-30 09:16:40

He escrito accidentalmente un {[100]]}

Haskell Functors Tutorial

Responderé a tu pregunta usando ejemplos, y pondré los tipos debajo en comentarios.

Cuidado con el patrón en los tipos.

fmap es una generalización de map

Los funtores son para darle la función fmap. fmap funciona como map, así que echemos un vistazo a map primero:

map (subtract 1) [2,4,8,16] = [1,3,7,15]
--    Int->Int     [Int]         [Int]

Por lo que utiliza la función (subtract 1) dentro de la lista. En hecho, para las listas, fmap hace exactamente lo que map hace. Vamos a multiplicar todo por 10 esta vez:

fmap (* 10)  [2,4,8,16] = [20,40,80,160]
--  Int->Int    [Int]         [Int]

Describiría esto como mapear la función que se multiplica por 10 sobre la lista.

fmap también funciona en Maybe

¿Qué más puedo fmap sobre? Usemos el tipo de datos Maybe, que tiene dos tipos de valores, Nothing y Just x. (Puede usar Nothing para representar un fallo al obtener una respuesta mientras que Just x representa una respuesta.)

fmap  (+7)    (Just 10)  = Just 17
fmap  (+7)     Nothing   = Nothing
--  Int->Int  Maybe Int    Maybe Int

OK, de nuevo, fmap está usando (+7) inside the Maybe. Y podemos fmap otras funciones también. length encuentra la longitud de una lista, por lo que podemos mapearla sobre Maybe [Double]

fmap    length             Nothing                      = Nothing
fmap    length    (Just [5.0, 4.0, 3.0, 2.0, 1.573458]) = Just 5
--  [Double]->Int         Maybe [Double]                  Maybe Int

En realidad length :: [a] -> Int pero lo estoy usando aquí en [Double] así que lo especialicé.

Usemos show para convertir cosas en cadenas. Secretamente el tipo real de show es Show a => a -> String, pero eso es un poco largo, y lo estoy usando aquí en un Int, por lo que se especializa en Int -> String.

fmap  show     (Just 12)  = Just "12"
fmap  show      Nothing   = Nothing
-- Int->String  Maybe Int   Maybe String

También, mirando hacia atrás a listas

fmap   show     [3,4,5] = ["3", "4", "5"]
-- Int->String   [Int]       [String]

fmap funciona en Either something

Usémoslo en una estructura ligeramente diferente, Either. Los valores de tipo Either a b son valores Left a o valores Right b. A veces usamos ya sea para representar un éxito Right goodvalue o un fracaso Left errordetails, y a veces solo para mezclar valores de dos tipos en uno. De todos modos, el funtor para ambos tipos de datos solo funciona en Right - deja solo los valores Left. Eso tiene sentido particularmente si está utilizando los valores correctos como el los exitosos (y de hecho no seríamos capaces de hacer que funcione en ambos porque los tipos no son necesariamente los mismos). Usemos el tipo Either String Int como ejemplo

fmap (5*)      (Left "hi")     =    Left "hi"
fmap (5*)      (Right 4)       =    Right 20
-- Int->Int  Either String Int   Either String Int

Hace que (5*) funcione dentro de Cualquiera de los Dos, pero para otros, solo se cambian los valores de Right. Pero podemos hacerlo al revés en Either Int String, siempre y cuando la función funcione en cadenas. Pongamos ", cool!" al final de stuff, usando (++ ", cool!").

fmap (++ ", cool!")          (Left 4)           = Left 4
fmap (++ ", cool!") (Right "fmap edits values") = Right "fmap edits values, cool!"
--   String->String    Either Int String          Either Int String

Es especialmente genial usar fmap en IO

Ahora una de mis formas favoritas de usar fmap es usarlo en valores IO para editar el valor que me da alguna operación IO. Hagamos un ejemplo que te permita escribir algo y luego imprimirlo de inmediato:

echo1 :: IO ()
echo1 = do
    putStrLn "Say something!"
    whattheysaid <- getLine  -- getLine :: IO String
    putStrLn whattheysaid    -- putStrLn :: String -> IO ()

Podemos escribir eso de una manera que me parezca más ordenada:{[100]]}

echo2 :: IO ()
echo2 = putStrLn "Say something" 
        >> getLine >>= putStrLn

>> hace una cosa tras otra, pero la razón por la que me gusta esto es porque >>= toma la Cadena que getLine nos dio y la alimenta a putStrLn que toma una Cadena. ¿Qué pasa si solo quería saludar al usuario:

greet1 :: IO ()
greet1 = do
    putStrLn "What's your name?"
    name <- getLine
    putStrLn ("Hello, " ++ name)

Si quisiéramos escribir eso de una manera más ordenada, estoy un poco atascado. Tendría que escribir

greet2 :: IO ()
greet2 = putStrLn "What's your name?" 
         >> getLine >>= (\name -> putStrLn ("Hello, " ++ name))

Que es no mejor que la versión do. De hecho, la notación do está ahí, así que no tienes que hacer esto. ¿Pero puede fmap venir al rescate? Sí puede. ("Hello, "++) es una función que puedo fmap sobre el getLine!

fmap ("Hello, " ++)  getLine   = -- read a line, return "Hello, " in front of it
--   String->String  IO String    IO String

Podemos usarlo así: {[100]]}

greet3 :: IO ()
greet3 = putStrLn "What's your name?" 
         >> fmap ("Hello, "++) getLine >>= putStrLn

Podemos hacer este truco con cualquier cosa que nos den. Vamos a estar en desacuerdo con si "Verdadero" o "Falso" fue escrito en:

fmap   not      readLn   = -- read a line that has a Bool on it, change it
--  Bool->Bool  IO Bool       IO Bool

O simplemente reportemos el tamaño de un archivo:

fmap  length    (readFile "test.txt") = -- read the file, return its length
--  String->Int      IO String              IO Int
--   [a]->Int        IO [Char]              IO Int     (more precisely)

Conclusiones: ¿Qué hace fmap y para qué lo hace?

Si ha estado observando los patrones en los tipos y pensando en los ejemplos, habrá notado que fmap toma una función que funciona en algunos valores y aplica esa función en algo que tiene o produce esos valores de alguna manera, editando los valores. (por ejemplo, readLn era leer Bool, así que tenía tipo IO Bool hay un valor booleano en él en el sentido de que produce un Bool, eg2 [4,5,6] tiene Int s en él.)

fmap :: (a -> b) -> Something a -> Something b

Esto funciona para Algo que es Lista-de (escrito []), Maybe, Either String, Either Int, IO y un montón de cosas. Lo llamamos un Funtor si esto funciona de una manera sensata (hay algunas reglas - más adelante). El tipo real de fmap es

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

Pero usualmente reemplazamos something con f para brevedad. Es todo lo mismo para el compilador, aunque:

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

Echa un vistazo a los tipos y comprueba que esto siempre funciona - cosa sobre Either String Int cuidadosamente - ¿qué es f esa vez?

Apéndice: ¿Cuáles son las reglas Funtor, y por qué las tenemos?

id es la función de identidad:

id :: a -> a
id x = x

Aquí están las reglas:

fmap id  ==  id                    -- identity identity
fmap (f . g)  ==  fmap f . fmap g  -- composition

En primer lugar la identidad identidad: Si mapeas la función que no hace nada, eso no cambia nada. Eso suena obvio (muchas reglas lo hacen), pero puedes interprete eso como diciendo que fmap es solo permitido cambiar los valores, no la estructura. A fmap no se le permite convertir Just 4 en Nothing, o [6] en [1,2,3,6], o Right 4 en Left 4 porque más que solo los datos cambiaron, la estructura o el contexto de esos datos cambiaron.

Golpeé esta regla una vez cuando estaba trabajando en un proyecto de interfaz gráfica de usuario - quería ser capaz de editar los valores, pero no podía hacerlo sin cambiar la estructura debajo. Nadie realmente habría notado la diferencia porque tenía el mismo efecto, pero darme cuenta de que no obedecía las reglas del functor me hizo replantearme todo mi diseño, y ahora es mucho más limpio, más pulido y más rápido.

En segundo lugar la composición: esto significa que puede elegir si desea fmap una función a la vez, o fmap ambas al mismo tiempo. Si fmap deja solo la estructura / contexto de sus valores y simplemente los edita con la función que se le ha dado, funcionará con esta regla demasiado.

¿por Qué los tenemos? Para asegurarnos de que fmap no haga nada a escondidas detrás de escena o cambie algo que no esperábamos. El compilador no las hace cumplir (pedirle al compilador que pruebe un teorema antes de compilar tu código no es justo, y ralentizaría la compilación - el programador debería comprobarlo). Esto significa que puedes hacer trampa, pero es un mal plan porque tu código puede dar resultados inesperados.

 108
Author: AndrewC,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-12-26 09:40:46

Es importante mantener separados en su cabeza la distinción entre un functor sí mismo, y un valor de un tipo que tiene un functor aplicada. Un funtor en sí es un constructor de tipo como Maybe, IO, o el constructor de lista []. Un valor en un funtor es un valor particular en un tipo con ese constructor de tipo aplicado. por ejemplo, Just 3 es un valor particular en el tipo Maybe Int (ese tipo es el funtor Maybe aplicado al tipo Int), putStrLn "Hello World" es un valor particular en el tipo IO (), y [2, 4, 8, 16, 32] es un valor particular en el tipo [Int].

Me gusta pensar que un valor en un tipo con un funtor aplicado es "el mismo" que un valor en el tipo base, pero con algún "contexto"adicional. La gente a menudo usa una analogía de contenedor para un funtor, que funciona bastante naturalmente para bastantes funtores, pero luego se convierte más en un obstáculo que en una ayuda cuando tienes que convencerte a ti mismo de que IO o (->) r es como un contenedor.

Así que si un Int representa un valor entero, entonces un Maybe Int representa un valor entero que puede no estar presente ("puede no estar presente" es el "contexto"). Un [Int] representa un valor entero con un número de valores posibles (esta es la misma interpretación del funtor de lista que la interpretación "nondeterminism" de la mónada de lista). Un IO Int representa un valor entero cuyo valor preciso depende de todo el universo (o alternativamente, representa un valor entero que se puede obtener ejecutando un proceso externo). A Char -> Int es un valor entero para cualquier Char valor ("función tomando r como argumento" es un funtor para cualquier tipo r; con r como Char (->) Char es el constructor de tipo que es un funtor, que aplicado a Int se convierte en (->) Char Int o Char -> Int en notación infijo).

Lo único que puedes hacer con un funtor general es fmap, con el tipo Functor f => (a -> b) -> (f a -> f b). fmap transforma una función que opera en valores normales en una función que opera en valores con contexto adicional agregado por un funtor; lo que hace exactamente esto es diferente para cada funtor, pero puedes hacerlo con todos ellos.

Así que con el funtor Maybe fmap (+1) es la función que calcula un entero posiblemente no presente 1 mayor que su entero de entrada posiblemente no presente. Con el functor de lista fmap (+1) es la función que calcula un entero no determinista 1 mayor que su entero no determinista de entrada. Con el funtor IO, fmap (+1) es la función que calcula un entero 1 mayor que su entrada entero-cuyo-valor-depende-del-universo-externo. Con el funtor (->) Char, fmap (+1) es la función que agrega 1 a un entero que depende de a Char (cuando alimento a Char al valor devuelto, obtengo 1 más alto de lo que habría obtenido al alimentar el mismo Char al valor original).

Pero en general, para algún funtor desconocidof, fmap (+1) aplicado a algún valor en f Int es la "versión de funtor" de la función (+1) en Int s. Agrega 1 al entero en cualquier tipo de "contexto" este funtor particular tiene.

Por sí solo, fmap no es necesariamente tan útil. Por lo general, cuando estás escribiendo un programa concreto y trabajando con un funtor, estás trabajando con un funtor en particular, y a menudo piensas en fmap como lo que sea que haga para ese funtor en particular. Cuando estoy trabajando con [Int], a menudo no estoy pensando en mis valores [Int] como enteros no deterministas, solo pienso en ellos como listas de enteros, y pienso en fmap de la misma manera que piensa en map.

Entonces, ¿por qué molestarse con los funtores? ¿Por qué no tener map para listas, applyToMaybe para Maybe s, y applyToIO para IO s? Entonces todos sabrían lo que hacen y nadie tendría que entender conceptos abstractos extraños como los funtores.

La clave es el reconocimiento de que hay un lote de funtores por ahí; casi todos los tipos de contenedores para empezar (de ahí la analogía del contenedor para lo que los funtores son). Cada uno de ellos tiene una operación correspondiente a fmap, incluso si no tenemos funtores. Cada vez que escribes un algoritmo únicamente en términos de la operación fmap (o map, o como se llame para tu tipo particular), entonces si lo escribes en términos de funtores en lugar de tu tipo particular, entonces funciona para todos los funtores.

También puede servir como una forma de documentación. Si entrego uno de mis valores de lista a una función que escribió que opera en listas, podría hacer cualquier número de cosas. Pero si entrego mi listar a una función que escribiste que opera en valores en un funtor arbitrario, entonces yo que la implementación de tu función no puede estar usando entidades de lista, solo entidades de funtor.

Pensar en cómo se usarían las cosas funcionales en la programación imperativa tradicional podría ayudar a ver los beneficios. Hay tipos de contenedores como arrays, listas, árboles, etc, generalmente tendrán algún patrón que use para iterar sobre ellos. Puede ser ligeramente diferente para diferentes contenedores, aunque las bibliotecas a menudo proporcionan interfaces de iteración estándar para abordar esto. Pero aún así terminas escribiendo un pequeño bucle for cada vez que quieres iterar sobre ellos, y cuando lo que quieres hacer es calcular un resultado para cada elemento en el contenedor y recopilar todos los resultados que normalmente terminas mezclando en la lógica para construir el nuevo contenedor a medida que avanzas.

fmap es cada para el bucle de esa forma que escribirás, ordenado de una vez por todas por los escritores de la biblioteca, antes de que te sientes a programar. Además, también se puede usar con cosas como Maybe y (->) r que probablemente no se consideraría que tuvieran nada que ver con el diseño de una interfaz de contenedor consistente en lenguajes imperativos.

 12
Author: Ben,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-31 00:27:34

En Haskell, los funtores capturan la noción de tener contenedores de "cosas", de modo que se puede manipular esa "cosa" sin cambiar la forma del contenedor.

Los funtores proporcionan una función, fmap, que le permite hacer esto, tomando una función regular y" elevándola " a una función desde contenedores de un tipo de elemento a otro:

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

Por ejemplo, [], el constructor de tipo de lista, es un funtor:

> fmap show [1, 2, 3]
["1","2","3"]

Y también lo son muchos otros tipos de Haskell los constructores, como Maybe y Map Integer1:

> fmap (+1) (Just 3)
Just 4
> fmap length (Data.Map.fromList [(1, "hi"), (2, "there")])
fromList [(1,2),(2,5)]

Tenga en cuenta que fmap no está permitido cambiar la "forma" del contenedor, por lo que si por ejemplo fmap una lista, el resultado tiene el mismo número de elementos, y si fmap a Just no puede convertirse en un Nothing. En términos formales, requerimos que fmap id = id, es decir, si fmap la función de identidad, nada cambia.

Hasta ahora he estado usando el término "contenedor", pero en realidad es un poco más general que eso. Por ejemplo, IO es también un funtor, y lo que queremos decir con "forma" en ese caso es que fmap en una acción IO no debe cambiar los efectos secundarios. De hecho, cualquier mónada es un functor2.

En teoría de categorías, los funtores te permiten convertir entre diferentes categorías, pero en Haskell solo tenemos realmente una categoría, a menudo llamada Hask. Por lo tanto, todos los funtores en Haskell convierten de Hask a Hask, por lo que son lo que llamamos endofunctors (funtores de una categoría a sí mismo).

En su la forma más simple, los funtores son algo aburridos. No hay mucho que puedas hacer con una sola operación. Sin embargo, una vez que empiezas a agregar operaciones, puedes ir de funtores regulares a funtores aplicativos a mónadas y las cosas rápidamente se vuelven mucho más interesantes, pero eso está más allá del alcance de esta respuesta.

1 Pero Set no lo es, porque solo puede almacenar tipos Ord. Los funtores deben poder contener cualquier tipo.
2 Debido a razones históricas, Functor no es una superclase de Monad, aunque mucha gente piensa que debería serlo.

 7
Author: hammar,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-10-30 09:32:59

Veamos los tipos.

Prelude> :i Functor
class Functor f where fmap :: (a -> b) -> f a -> f b

Pero, ¿qué significa eso?

Primero, f es una variable de tipo aquí, y representa un constructor de tipo: f a es un tipo; a es una variable de tipo que representa algún tipo.

En segundo lugar, dada una función g :: a -> b, obtendrás fmap g :: f a -> f b. Es decir, fmap g es una función que transforma las cosas de tipo f a en cosas de tipo f b. Observe, no podemos llegar a las cosas de tipo a ni b aquí. La función g :: a -> b de alguna manera está hecha para trabajar en cosas de tipo f a y transformarlas en cosas de tipo f b.

Observe que f es lo mismo. Solo el otro tipo cambia.

¿Qué significa eso? Puede significar muchas cosas. f generalmente se ve como "contenedor" de cosas. Entonces, fmap g permite a g actuar en el interior de estos contenedores, sin romperlos. Los resultados todavía están encerrados "dentro", la clase de tipo Functor no nos proporciona habilidades para abrirlos, o mirar dentro. Sólo una transformación dentro de cosas opacas es todo lo que tenemos. Cualquier otra funcionalidad tendrá que venir de otro lugar.

También tenga en cuenta que no dice que estos "contenedores" llevan solo una "cosa" de tipo a; puede haber muchas "cosas" separadas "dentro" de él, pero todas del mismo tipo a.

Por último, cualquier candidato a un funtor debe obedecer las leyes del funtor :

fmap id === id
fmap (f . g) === fmap f . fmap g
 3
Author: Will Ness,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-07 15:13:12