Haskell Language
Functor
Buscar..
Introducción
Functor
es la clase de tipos f :: * -> *
que se puede mapear de forma covariante. El mapeo de una función sobre una estructura de datos aplica la función a todos los elementos de la estructura sin cambiar la estructura en sí.
Observaciones
Un Functor puede considerarse como un contenedor para algún valor, o un contexto de cómputo. Algunos ejemplos son Maybe a
o [a]
. El artículo de Typeclassopedia tiene una buena reseña de los conceptos detrás de los Functors.
Para ser considerado un Functor real, una instancia debe respetar las 2 leyes siguientes:
Identidad
fmap id == id
Composición
fmap (f . g) = (fmap f) . (fmap g)
Instancias comunes de Functor
Tal vez
Maybe
es un Functor
contiene un valor posiblemente ausente:
instance Functor Maybe where
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)
La instancia de Functor
de Maybe
aplica una función a un valor envuelto en un Just
. Si el cálculo ha fallado anteriormente (por lo que el valor Maybe
es un Nothing
), entonces no hay ningún valor para aplicar la función, por lo que fmap
es un no-op.
> fmap (+ 3) (Just 3)
Just 6
> fmap length (Just "mousetrap")
Just 9
> fmap sqrt Nothing
Nothing
Podemos verificar las leyes de los funtores para esta instancia usando el razonamiento ecuacional. Por la ley de identidad,
fmap id Nothing
Nothing -- definition of fmap
id Nothing -- definition of id
fmap id (Just x)
Just (id x) -- definition of fmap
Just x -- definition of id
id (Just x) -- definition of id
Por la ley de composición,
(fmap f . fmap g) Nothing
fmap f (fmap g Nothing) -- definition of (.)
fmap f Nothing -- definition of fmap
Nothing -- definition of fmap
fmap (f . g) Nothing -- because Nothing = fmap f Nothing, for all f
(fmap f . fmap g) (Just x)
fmap f (fmap g (Just x)) -- definition of (.)
fmap f (Just (g x)) -- definition of fmap
Just (f (g x)) -- definition of fmap
Just ((f . g) x) -- definition of (.)
fmap (f . g) (Just x) -- definition of fmap
Liza
La instancia de lista de Functor
aplica la función a cada valor de la lista en su lugar.
instance Functor [] where
fmap f [] = []
fmap f (x:xs) = f x : fmap f xs
Esto también podría escribirse como una lista de comprensión: fmap f xs = [fx | x <- xs]
.
Este ejemplo muestra que fmap
generaliza el map
. map
solo funciona en listas, mientras que fmap
funciona en un Functor
arbitrario.
Se puede demostrar que la ley de identidad se cumple por inducción:
-- base case
fmap id []
[] -- definition of fmap
id [] -- definition of id
-- inductive step
fmap id (x:xs)
id x : fmap id xs -- definition of fmap
x : fmap id xs -- definition of id
x : id xs -- by the inductive hypothesis
x : xs -- definition of id
id (x : xs) -- definition of id
y similarmente, la ley de composición:
-- base case
(fmap f . fmap g) []
fmap f (fmap g []) -- definition of (.)
fmap f [] -- definition of fmap
[] -- definition of fmap
fmap (f . g) [] -- because [] = fmap f [], for all f
-- inductive step
(fmap f . fmap g) (x:xs)
fmap f (fmap g (x:xs)) -- definition of (.)
fmap f (g x : fmap g xs) -- definition of fmap
f (g x) : fmap f (fmap g xs) -- definition of fmap
(f . g) x : fmap f (fmap g xs) -- definition of (.)
(f . g) x : fmap (f . g) xs -- by the inductive hypothesis
fmap (f . g) xs -- definition of fmap
Funciones
No todos los Functor
parecen un contenedor. La instancia de Funciones de Functor
aplica una función al valor de retorno de otra función.
instance Functor ((->) r) where
fmap f g = \x -> f (g x)
Tenga en cuenta que esta definición es equivalente a fmap = (.)
. Así fmap
generaliza la composición de funciones.
Una vez más comprobando la ley de identidad:
fmap id g
\x -> id (g x) -- definition of fmap
\x -> g x -- definition of id
g -- eta-reduction
id g -- definition of id
y la ley de composición:
(fmap f . fmap g) h
fmap f (fmap g h) -- definition of (.)
fmap f (\x -> g (h x)) -- definition of fmap
\y -> f ((\x -> g (h x)) y) -- definition of fmap
\y -> f (g (h y)) -- beta-reduction
\y -> (f . g) (h y) -- definition of (.)
fmap (f . g) h -- definition of fmap
Definición de clase de Functor y Leyes
class Functor f where
fmap :: (a -> b) -> f a -> f b
Una forma de verlo es que fmap
eleva una función de valores a una función de valores en un contexto f
.
Una instancia correcta de Functor
debería satisfacer las leyes de los funtores , aunque el compilador no las impone:
fmap id = id -- identity
fmap f . fmap g = fmap (f . g) -- composition
Hay un alias de infijo de uso fmap
para fmap
llamado <$>
.
infixl 4 <$>
(<$>) :: Functor f => (a -> b) -> f a -> f b
(<$>) = fmap
Reemplazo de todos los elementos de un Functor con un solo valor
El módulo Data.Functor
contiene dos combinadores, <$
y $>
, que ignoran todos los valores contenidos en un functor, reemplazándolos con un único valor constante.
infixl 4 <$, $>
<$ :: Functor f => a -> f b -> f a
(<$) = fmap . const
$> :: Functor f => f a -> b -> f b
($>) = flip (<$)
void
ignora el valor de retorno de un cálculo.
void :: Functor f => f a -> f ()
void = (() <$)
Funtores polinomiales
Hay un conjunto útil de combinadores de tipo para construir grandes Functor
s a partir de los más pequeños. Estos son instructivos como ejemplos de Functor
, y también son útiles como una técnica para la programación genérica, porque se pueden usar para representar una gran clase de functores comunes.
El functor de la identidad.
El functor de la identidad simplemente envuelve su argumento. Es una implementación a nivel de tipo del combinador I
del cálculo de SKI.
newtype I a = I a
instance Functor I where
fmap f (I x) = I (f x)
I
pueden encontrar, bajo el nombre de Identity
, en el módulo Data.Functor.Identity
.
El functor constante
El functor constante ignora su segundo argumento, que contiene solo un valor constante. Es un análogo de nivel de tipo de const
, el combinador K
del cálculo de SKI.
newtype K c a = K c
Tenga en cuenta que K ca
no contiene a
-valores; K ()
es isomorfo a Proxy
. Esto significa que K
implementación de 's fmap
no hace ninguna asignación en absoluto!
instance Functor (K c) where
fmap _ (K c) = K c
K
también se conoce como Const
, de Data.Functor.Const
.
Los functores restantes en este ejemplo combinan funtores más pequeños en otros más grandes.
Productos funcionales
El producto functor toma un par de functores y los empaca. Es análogo a una tupla, excepto que while (,) :: * -> * -> *
opera en types
*
, (:*:) :: (* -> *) -> (* -> *) -> (* -> *)
opera en los functors
* -> *
.
infixl 7 :*:
data (f :*: g) a = f a :*: g a
instance (Functor f, Functor g) => Functor (f :*: g) where
fmap f (fx :*: gy) = fmap f fx :*: fmap f gy
Este tipo se puede encontrar, bajo el nombre Product
, en el módulo Data.Functor.Product
.
Functor coproductos
Al igual que :*:
es análogo a (,)
:+:
es el análogo a nivel de functor de Either
de los Either
.
infixl 6 :+:
data (f :+: g) a = InL (f a) | InR (g a)
instance (Functor f, Functor g) => Functor (f :+: g) where
fmap f (InL fx) = InL (fmap f fx)
fmap f (InR gy) = InR (fmap f gy)
:+:
se puede encontrar bajo el nombre Sum
, en el módulo Data.Functor.Sum
.
Composición funcional
Finalmente, :.:
Funciona como un nivel de tipo (.)
, Tomando la salida de un functor y conectándola a la entrada de otra.
infixr 9 :.:
newtype (f :.: g) a = Cmp (f (g a))
instance (Functor f, Functor g) => Functor (f :.: g) where
fmap f (Cmp fgx) = Cmp (fmap (fmap f) fgx)
El tipo de Compose
se puede encontrar en Data.Functor.Compose
Funtores polinómicos para la programación genérica.
I
, K
:*:
, :+:
y :.:
Se puede considerar como un kit de bloques de construcción para una cierta clase de tipos de datos simples. El kit se vuelve especialmente poderoso cuando lo combinas con puntos fijos porque los tipos de datos construidos con estos combinadores son automáticamente instancias de Functor
. Usted usa el kit para crear un tipo de plantilla, marcando puntos recursivos con I
, y luego lo conecta a Fix
para obtener un tipo que se puede usar con el zoológico estándar de esquemas de recursión.
Nombre | Como un tipo de datos | Usando el kit de functor |
---|---|---|
Pares de valores | data Pair a = Pair aa | type Pair = I :*: I |
Grid de dos por dos | type Grid a = Pair (Pair a) | type Grid = Pair :.: Pair |
Números naturales | data Nat = Zero | Succ Nat | type Nat = Fix (K () :+: I) |
Liza | data List a = Nil | Cons a (List a) | type List a = Fix (K () :+: K a :*: I) |
Árboles binarios | data Tree a = Leaf | Node (Tree a) a (Tree a) | type Tree a = Fix (K () :+: I :*: K a :*: I) |
Rosales | data Rose a = Rose a (List (Rose a)) | type Rose a = Fix (K a :*: List :.: I) |
Este enfoque de "kit" para diseñar tipos de datos es la idea detrás de las bibliotecas de programación genéricas como generics-sop
. La idea es escribir operaciones genéricas usando un kit como el que se presentó anteriormente, y luego usar una clase de tipos para convertir tipos de datos arbitrarios desde y hacia su representación genérica:
class Generic a where
type Rep a -- a generic representation built using a kit
to :: a -> Rep a
from :: Rep a -> a
Functors en la teoría de categorías
Un Functor se define en la teoría de categorías como un mapa que preserva la estructura (un 'homomorfismo') entre categorías. Específicamente, (todos) los objetos se asignan a los objetos, y (todas) las flechas se asignan a las flechas, de manera que se conservan las leyes de la categoría.
La categoría en la que los objetos son tipos de Haskell y los morfismos en funciones de Haskell se denomina Hask . Entonces, un functor de Hask a Hask consistiría en un mapeo de tipos a tipos y un mapeo de funciones a funciones.
La relación que este concepto teórico de la categoría guarda con la estructura de programación de Haskell, Functor
es bastante directa. La asignación de tipos a tipos toma la forma de un tipo f :: * -> *
, y la asignación de funciones a funciones toma la forma de una función fmap :: (a -> b) -> (fa -> fb)
. Poniéndolos juntos en una clase,
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
fmap
es una operación que toma una función (un tipo de morfismo), :: a -> b
, y la asigna a otra función, :: fa -> fb
. Se supone (pero se deja al programador para garantizar) que las instancias de Functor
son en realidad functors matemáticos, preservando la estructura categórica de Hask :
fmap (id {- :: a -> a -}) == id {- :: f a -> f a -}
fmap (h . g) == fmap h . fmap g
fmap
levanta una función :: a -> b
en una subcategoría de Hask de una manera que preserva tanto la existencia de flechas de identidad como la asociatividad de la composición.
La clase Functor
solo codifica los functores endo en Hask . Pero en matemáticas, los funtores pueden mapear entre categorías arbitrarias. Una codificación más fiel de este concepto se vería así:
class Category c where
id :: c i i
(.) :: c j k -> c i j -> c i k
class (Category c1, Category c2) => CFunctor c1 c2 f where
cfmap :: c1 a b -> c2 (f a) (f b)
La clase de Functor estándar es un caso especial de esta clase en la que las categorías de origen y destino son Hask . Por ejemplo,
instance Category (->) where -- Hask
id = \x -> x
f . g = \x -> f (g x)
instance CFunctor (->) (->) [] where
cfmap = fmap
Functor de derivación
La extensión de lenguaje DeriveFunctor
permite a GHC generar instancias de Functor
automáticamente.
{-# LANGUAGE DeriveFunctor #-}
data List a = Nil | Cons a (List a) deriving Functor
-- instance Functor List where -- automatically defined
-- fmap f Nil = Nil
-- fmap f (Cons x xs) = Cons (f x) (fmap f xs)
map :: (a -> b) -> List a -> List b
map = fmap