monadas - que hace en haskell
Comprender cómo O bien es una instancia de Functor (4)
Ahora, estoy tratando de entender por qué la implementación se correlaciona en el caso de un constructor de valor Derecho, pero no en el caso de un Izquierda?
Enchúfalo aquí y podría tener sentido.
Suponga que a = String (un mensaje de error) Usted aplica O a a Float.
Entonces tienes una f: Flotante -> Entero, por ejemplo redondeo.
(Either String) (Float) = Cualquiera de String Float.
ahora (fmap f) :: O String Float -> O String Int Entonces, ¿qué vas a hacer con f? f no tiene ni idea de qué hacer con las cadenas para que no pueda hacer nada allí. Es obvio que lo único en lo que puedes actuar son los valores correctos sin modificar los valores de la izquierda.
En otras palabras, O a es un funtor porque existe una fmap tan obvia dada por:
- para valores correctos aplicar f
- para valores Left no hacen nada
En mi tiempo libre, estoy aprendiendo Haskell, así que esta es una pregunta para principiantes.
En mis lecturas me encontré con un ejemplo que ilustra cómo se hace una de las dos instancias de Functor
:
instance Functor (Either a) where
fmap f (Right x) = Right (f x)
fmap f (Left x) = Left x
Ahora, estoy tratando de entender por qué la implementación se correlaciona en el caso de un constructor de valor Right
, pero no en el caso de un Left
?
Aquí está mi entendimiento:
Primero déjame reescribir la instancia anterior como
instance Functor (Either a) where
fmap g (Right x) = Right (g x)
fmap g (Left x) = Left x
Ahora:
Sé que
fmap :: (c -> d) -> fc -> fd
si sustituimos
f
porEither a
obtenemosfmap :: (c -> d) -> Either ac -> Either ad
el tipo de
Right (gx)
esEither a (gx)
, y el tipo degx
esd
, entonces tenemos que el tipo deRight (gx)
esEither ad
, que es lo que esperamos defmap
(ver 2. más arriba)ahora, si miramos a la
Left (gx)
podemos usar el mismo razonamiento para decir que su tipo esEither (gx) b
, es decir,Either db
, que no es lo que esperamos defmap
(ver 2. arriba): eld
debería ser el segundo parámetro, ¡no el primero! Entonces no podemos mapear sobreLeft
.
¿Mi razonamiento es correcto?
Como otros mencionaron, Either
tipos es un funtor en sus dos argumentos. Pero en Haskell somos capaces de (directamente) definir solo funtores en los últimos argumentos de un tipo. En casos como este, podemos evitar la limitación mediante el uso de newtype
s:
newtype FlipEither b a = FlipEither { unFlipEither :: Either a b }
Así que tenemos el constructor FlipEither :: Either ab -> FlipEither ba
que envuelve un Either
en nuestro newtype
tipo con argumentos de tipo intercambiado. Y tenemos declabctor unFlipEither :: FlipEither ba -> Either ab
que lo desenrolla. Ahora podemos definir una instancia de functor en el último argumento de FlipEither
, que en realidad es el primer argumento de Either
:
instance Functor (FlipEither b) where
fmap f (FlipEither (Left x)) = FlipEither (Left (f x))
fmap f (FlipEither (Right x)) = FlipEither (Right x)
Tenga en cuenta que si olvidamos FlipEither
por un tiempo obtenemos la definición de Functor
para Either
, solo con Left
/ Right
intercambiada. Y ahora, cada vez que necesitamos una instancia de Functor
en el primer argumento de tipo de Either
, podemos ajustar el valor en FlipEither
y desenvolverlo después. Por ejemplo:
fmapE2 :: (a -> b) -> Either a c -> Either b c
fmapE2 f = unFlipEither . fmap f . FlipEither
Actualización: Eche un vistazo a Data.Bifunctor , del cual Either
y (,)
son instancias de. Cada bifunctor tiene dos argumentos y es un functor en cada uno de ellos. Esto se refleja en los métodos de Bifunctor
first
y second
.
La definición de Bifunctor
de Either
es muy simétrica:
instance Bifunctor Either where
bimap f _ (Left a) = Left (f a)
bimap _ g (Right b) = Right (g b)
first f = bimap f id
second f = bimap id f
Esto es correcto. También hay otra razón bastante importante para este comportamiento: puede pensar en Either ab
como un cálculo, que puede tener éxito y retornar o fallar con un mensaje de error a
. (Esta es también, cómo funciona la instancia de la mónada). Entonces, es natural que la instancia del funtor no toque los valores de la Left
, ya que desea mapear sobre el cálculo, si falla, no hay nada que manipular.
Su cuenta es correcta, por supuesto. Tal vez la razón por la que tenemos una dificultad con instancias como esta es que estamos definiendo infinitamente muchas instancias funcionadoras a la vez, una para cada posible tipo Left
. Pero una instancia de Functor es una forma sistemática de operar en infinitos tipos en el sistema. Así que estamos definiendo infinitamente muchas formas de operar sistemáticamente en infinitos tipos en el sistema. La instancia implica generalidad de dos maneras.
Si lo tomas por etapas, tal vez no sea tan extraño. El primero de estos tipos es una versión larga de Maybe
usando el tipo de unidad ()
y su único valor legítimo ()
:
data MightBe b = Nope () | Yep b
data UnlessError b = Bad String | Good b
data ElseInt b = Else Int | Value b
Aquí podríamos cansarnos y hacer una abstracción:
data Unless a b = Mere a | Genuine b
Ahora hacemos nuestras instancias de Functor, sin problemas, la primera que se parece mucho a la instancia de Maybe
:
instance Functor MightBe where
fmap f (Nope ()) = Nope () -- compare with Nothing
fmap f (Yep x) = Yep (f x) -- compare with Just (f x)
instance Functor UnlessError where
fmap f (Bad str) = Bad str -- a more informative Nothing
fmap f (Good x) = Good (f x)
instance Functor ElseInt where
fmap f (Else n) = Else n
fmap f (Value b) = Value (f b)
Pero, de nuevo, ¿por qué molestarse, hagamos la abstracción:
instance Functor (Unless a) where
fmap f (Mere a) = Mere a
fmap f (Genuine x) = Genuine (f x)
Los términos Mere a
no se tocan, ya que los valores ()
, String
e Int
no se tocaron.