haskell - ¿Por qué los funtores aplicativos pueden tener efectos secundarios, pero los funtores no pueden?
monads side-effects (4)
Me siento bastante tonto al hacer esta pregunta, pero ha estado en mi mente por un tiempo y no puedo encontrar ninguna respuesta.
Entonces la pregunta es: ¿por qué los funtores aplicativos pueden tener efectos secundarios, pero los funtores no pueden?
Tal vez puedan y yo nunca me haya dado cuenta ...?
Esta respuesta es un poco simplista, pero si definimos los efectos secundarios como los cálculos que se ven afectados por los cálculos anteriores, es fácil ver que la clase de tipos de Functor
es insuficiente para los efectos secundarios simplemente porque no hay manera de encadenar múltiples cálculos.
class Functor f where
fmap :: (a -> b) -> f a -> f b
Lo único que puede hacer un functor es alterar el resultado final de un cálculo a través de alguna función pura a -> b
.
Sin embargo, un functor aplicativo agrega dos funciones nuevas, pure
y <*>
.
class Functor f => Applicative f where
pure :: a -> f a
(<*>) :: f (a -> b) -> f a -> f b
La <*>
es la diferencia crucial aquí, ya que nos permite encadenar dos cálculos: f (a -> b)
(un cálculo que produce una función) y un cálculo que proporciona el parámetro al que se aplica la función. Usando pure
y <*>
es posible definir, por ejemplo,
(*>) :: f a -> f b -> f b
Que simplemente encadena dos cálculos, descartando el resultado final del primero (pero posiblemente aplicando "efectos secundarios").
En resumen, la capacidad de los cálculos en cadena es el requisito mínimo para efectos tales como el estado mutable en los cálculos.
No es cierto que los Functor
no tengan efectos. Cada Applicative
(y cada Monad
través de WrappedMonad
) es un Functor
. La principal diferencia es que Applicative
y Monad
brindan herramientas sobre cómo trabajar con esos efectos, cómo combinarlos. Aproximadamente
-
Applicative
te permite secuenciar efectos y combinar valores dentro. - Además, la
Monad
te permite determinar el siguiente efecto según el resultado de uno anterior.
Sin embargo, Functor
solo le permite modificar el valor interior, no proporciona herramientas para hacer nada con el efecto. Entonces, si algo es solo Functor
y no Applicative
, no significa que no tenga efectos. Simplemente no tiene un mecanismo para combinarlos de esta manera.
Actualización: Como ejemplo, considere
import Control.Applicative
newtype MyF r a = MyF (IO (r, a))
instance Functor (MyF r) where
fmap f (MyF x) = MyF $ fmap (fmap f) x
Esto es claramente una instancia de Functor
que lleva efectos. Es solo que no tenemos una manera de definir las operaciones con estos efectos que cumplirían con Applicative
. A menos que impongamos algunas restricciones adicionales a r
, no hay forma de definir una instancia de Applicative
.
Otras respuestas aquí han indicado con razón que los funtores no permiten los efectos secundarios porque no pueden combinarse o secuenciarse, lo cual es bastante cierto en general, pero hay una manera de secuenciar los funtores: ir hacia adentro.
Vamos a escribir un functor de escritor limitado.
data Color = R | G | B
data ColorW a = Re a | Gr a | Bl a deriving (Functor)
y luego aplicar el tipo de mónada libre a él
data Free f a = Pure a | Free (f (Free f a))
liftF :: Functor f => f a -> Free f a
liftF = Free . fmap Pure
type ColorWriter = Free ColorW
red, blue, green :: a -> ColorWriter a
red = liftF . Re
green = liftF . Gr
blue = liftF . Bl
Por supuesto, por la propiedad libre, esto forma una mónada, pero los efectos realmente vienen de las "capas" del functor.
interpretColors :: ColorWriter a -> ([Color], a)
interpretColors (Pure a) = ([], a)
interpretColors (Free (Re next)) = let (colors, a) = interpretColors next
in (R : colors, a)
...
Por lo tanto, esto es una especie de truco. Realmente el "cálculo" está siendo introducido por la mónada libre, pero el material de la computación, el contexto oculto, es introducido solo por un funtor. Resulta que puedes hacer esto con cualquier tipo de datos, ni siquiera tiene que ser un Functor, pero Functor proporciona una forma clara de construirlo.
Primero vamos a cambiar el nombre de los efectos secundarios a los efectos . Todo tipo de valores pueden tener efectos. Un functor es un tipo que le permite mapear una función sobre lo que sea producido por ese efecto.
Cuando un funtor no es aplicativo, no le permite usar un cierto estilo de composición para los efectos. Vamos a elegir un ejemplo (ideado):
data Contrived :: * -> * where
AnInt :: Int -> Contrived Int
ABool :: Bool -> Contrived Bool
None :: Contrived a
Esto es fácilmente un functor:
instance Functor Contrived where
fmap f (AnInt x) = AnInt (f x)
fmap f (ABool x) = ABool (f x)
fmap _ None = None
Sin embargo, no existe una implementación sensata para pure
, por lo que este tipo no es un functor aplicativo. Es similar a Maybe
porque tiene el efecto de que puede no haber un valor de resultado. Pero no se puede componer utilizando combinadores aplicativos.