Aquí va un argumento que apoya ampliamente su hermosa idea.
Primera parte: mapMaybe
Mi plan aquí es replantear el problema en términos de mapMaybe
, con la esperanza de que hacerlo nos lleve a un terreno más familiar. Para hacerlo, Either
usaré algunas funciones de utilidad que cambian:
maybeToRight :: a -> Maybe b -> Either a b
rightToMaybe :: Either a b -> Maybe b
leftToMaybe :: Either a b -> Maybe a
flipEither :: Either a b -> Either b a
(Tomé los primeros tres nombres de relude , y el cuarto de errores . Por cierto, ofertas de erroresmaybeToRight
y rightToMaybe
como note
y hush
respectivamente, en Control.Error.Util
).
Como notó, mapMaybe
se puede definir en términos de partition
:
mapMaybe :: Filterable f => (a -> Maybe b) -> f a -> f b
mapMaybe f = snd . partition . fmap (maybeToRight () . f)
De manera crucial, también podemos dar la vuelta:
partition :: Filterable f => f (Either a b) -> (f a, f b)
partition = mapMaybe leftToMaybe &&& mapMaybe rightToMaybe
Esto sugiere que tiene sentido reformular sus leyes en términos de mapMaybe
. Con las leyes de identidad, hacerlo nos da una gran excusa para olvidarnos por completo de trivial
:
-- Left and right unit
mapMaybe rightToMaybe . fmap (bwd elunit) = id -- [I]
mapMaybe leftToMaybe . fmap (bwd erunit) = id -- [II]
En cuanto a la asociatividad, podemos usar rightToMaybe
y leftToMaybe
dividir la ley en tres ecuaciones, una para cada componente que obtenemos de las particiones sucesivas:
-- Associativity
mapMaybe rightToMaybe . fmap (bwd eassoc)
= mapMaybe rightToMaybe . mapMaybe rightToMaybe -- [III]
mapMaybe rightToMaybe . mapMaybe leftToMaybe . fmap (bwd eassoc)
= mapMaybe leftToMaybe . mapMaybe rightToMaybe -- [IV]
mapMaybe leftToMaybe . fmap (bwd eassoc)
= mapMaybe leftToMaybe . mapMaybe leftToMaybe -- [V]
Parametricity significa mapMaybe
es agnóstico con respecto a los Either
valores que estamos tratando aquí. Siendo así, podemos usar nuestro pequeño arsenal de Either
isomorfismos para barajar las cosas y mostrar que [I] es equivalente a [II], y [III] es equivalente a [V]. Ahora tenemos tres ecuaciones:
mapMaybe rightToMaybe . fmap (bwd elunit) = id -- [I]
mapMaybe rightToMaybe . fmap (bwd eassoc)
= mapMaybe rightToMaybe . mapMaybe rightToMaybe -- [III]
mapMaybe rightToMaybe . mapMaybe leftToMaybe . fmap (bwd eassoc)
= mapMaybe leftToMaybe . mapMaybe rightToMaybe -- [IV]
La parametricidad nos permite tragar la fmap
in [I]:
mapMaybe (rightToMaybe . bwd elunit) = id
Eso, sin embargo, es simplemente ...
mapMaybe Just = id
... que es equivalente a la ley de conservación / identidad de Witherable 'sFilterable
:
mapMaybe (Just . f) = fmap f
Eso Filterable
también tiene una ley de composición:
-- The (<=<) is from the Maybe monad.
mapMaybe g . mapMaybe f = mapMaybe (g <=< f)
¿Podemos también derivar esto de nuestras leyes? Comencemos desde [III] y, una vez más, hagamos que la parametricidad haga su trabajo. Este es más complicado, así que lo escribiré en su totalidad:
mapMaybe rightToMaybe . fmap (bwd eassoc)
= mapMaybe rightToMaybe . mapMaybe rightToMaybe -- [III]
-- f :: a -> Maybe b; g :: b -> Maybe c
-- Precomposing fmap (right (maybeToRight () . g) . maybeToRight () . f)
-- on both sides:
mapMaybe rightToMaybe . fmap (bwd eassoc)
. fmap (right (maybeToRight () . g) . maybeToRight () . f)
= mapMaybe rightToMaybe . mapMaybe rightToMaybe
. fmap (right (maybeToRight () . g) . maybeToRight () . f)
mapMaybe rightToMaybe . mapMaybe rightToMaybe
. fmap (right (maybeToRight () . g) . maybeToRight () . f) -- RHS
mapMaybe rightToMaybe . fmap (maybeToRight () . g)
. mapMaybe rightToMaybe . fmap (maybeToRight () . f)
mapMaybe (rightToMaybe . maybeToRight () . g)
. mapMaybe (rightToMaybe . maybeToRight () . f)
mapMaybe g . mapMaybe f
mapMaybe rightToMaybe . fmap (bwd eassoc)
. fmap (right (maybeToRight () . g) . maybeToRight () . f) -- LHS
mapMaybe (rightToMaybe . bwd eassoc
. right (maybeToRight () . g) . maybeToRight () . f)
mapMaybe (rightToMaybe . bwd eassoc
. right (maybeToRight ()) . maybeToRight () . fmap @Maybe g . f)
-- join @Maybe
-- = rightToMaybe . bwd eassoc . right (maybeToRight ()) . maybeToRight ()
mapMaybe (join @Maybe . fmap @Maybe g . f)
mapMaybe (g <=< f) -- mapMaybe (g <=< f) = mapMaybe g . mapMaybe f
En la otra dirección:
mapMaybe (g <=< f) = mapMaybe g . mapMaybe f
-- f = rightToMaybe; g = rightToMaybe
mapMaybe (rightToMaybe <=< rightToMaybe)
= mapMaybe rightToMaybe . mapMaybe rightToMaybe
mapMaybe (rightToMaybe <=< rightToMaybe) -- LHS
mapMaybe (join @Maybe . fmap @Maybe rightToMaybe . rightToMaybe)
-- join @Maybe
-- = rightToMaybe . bwd eassoc . right (maybeToRight ()) . maybeToRight ()
mapMaybe (rightToMaybe . bwd eassoc
. right (maybeToRight ()) . maybeToRight ()
. fmap @Maybe rightToMaybe . rightToMaybe)
mapMaybe (rightToMaybe . bwd eassoc
. right (maybeToRight () . rightToMaybe)
. maybeToRight () . rightToMaybe)
mapMaybe (rightToMaybe . bwd eassoc) -- See note below.
mapMaybe rightToMaybe . fmap (bwd eassoc)
-- mapMaybe rightToMaybe . fmap (bwd eassoc)
-- = mapMaybe rightToMaybe . mapMaybe rightToMaybe
(Nota: mientras maybeToRight () . rightToMaybe :: Either a b -> Either () b
no lo esté id
, en la derivación arriba los valores de la izquierda se descartarán de todos modos, por lo que es justo tacharlo como si lo fuera id
).
Por lo tanto, [III] es equivalente a la ley de composición de witherable 's Filterable
.
En este punto, podemos usar la ley de composición para tratar [IV]:
mapMaybe rightToMaybe . mapMaybe leftToMaybe . fmap (bwd eassoc)
= mapMaybe leftToMaybe . mapMaybe rightToMaybe -- [IV]
mapMaybe (rightToMaybe <=< leftToMaybe) . fmap (bwd eassoc)
= mapMaybe (letfToMaybe <=< rightToMaybe)
mapMaybe (rightToMaybe <=< leftToMaybe . bwd eassoc)
= mapMaybe (letfToMaybe <=< rightToMaybe)
-- Sufficient condition:
rightToMaybe <=< leftToMaybe . bwd eassoc = letfToMaybe <=< rightToMaybe
-- The condition holds, as can be directly verified by substiuting the definitions.
Esto es suficiente para mostrar que su clase equivale a una formulación bien establecida de Filterable
, que es un resultado muy bueno. Aquí hay un resumen de las leyes:
mapMaybe Just = id -- Identity
mapMaybe g . mapMaybe f = mapMaybe (g <=< f) -- Composition
Como señalan los doctores , estas son leyes de functor para un functor desde Kleisli Maybe hasta Hask .
Segunda parte: alternativa y mónada
Ahora podemos abordar su pregunta real, que era sobre mónadas alternativas. Su implementación propuesta de partition
fue:
partitionAM :: (Alternative f, Monad f) => f (Either a b) -> (f a, f b)
partitionAM
= (either return (const empty) =<<) &&& (either (const empty) return =<<)
Siguiendo mi plan más amplio, pasaré a la mapMaybe
presentación:
mapMaybe f
snd . partition . fmap (maybeToRight () . f)
snd . (either return (const empty) =<<) &&& (either (const empty) return =<<)
. fmap (maybeToRight () . f)
(either (const empty) return =<<) . fmap (maybeToRight () . f)
(either (const empty) return . maybeToRight . f =<<)
(maybe empty return . f =<<)
Y así podemos definir:
mapMaybeAM :: (Alternative f, Monad f) => (a -> Maybe b) -> f a -> f b
mapMaybeAM f u = maybe empty return . f =<< u
O, en una ortografía sin puntos:
mapMaybeAM = (=<<) . (maybe empty return .)
Algunos párrafos anteriores, noté que las Filterable
leyes dicen que mapMaybe
es el mapeo de morfismo de un functor desde Kleisli Maybe hasta Hask . Dado que la composición de los functores es un functor, y (=<<)
el mapeo de morfismo de un functor de Kleisli f a Hask , (maybe empty return .)
el mapeo de morfismo de un functor de Kleisli Quizás a Kleisli f es suficiente para mapMaybeAM
ser legal. Las leyes de functor relevantes son:
maybe empty return . Just = return -- Identity
maybe empty return . g <=< maybe empty return . f
= maybe empty return . (g <=< f) -- Composition
Esta ley de identidad es válida, así que centrémonos en la composición uno:
maybe empty return . g <=< maybe empty return . f
= maybe empty return . (g <=< f)
maybe empty return . g =<< maybe empty return (f a)
= maybe empty return (g =<< f a)
-- Case 1: f a = Nothing
maybe empty return . g =<< maybe empty return Nothing
= maybe empty return (g =<< Nothing)
maybe empty return . g =<< empty = maybe empty return Nothing
maybe empty return . g =<< empty = empty -- To be continued.
-- Case 2: f a = Just b
maybe empty return . g =<< maybe empty return (Just b)
= maybe empty return (g =<< Just b)
maybe empty return . g =<< return b = maybe empty return (g b)
maybe empty return (g b) = maybe empty return (g b) -- OK.
Por lo tanto, mapMaybeAM
es legal si es maybe empty return . g =<< empty = empty
por alguno g
. Ahora, si empty
se define como absurd <$> nil ()
, como lo ha hecho aquí, podemos probarlo f =<< empty = empty
para cualquier f
:
f =<< empty = empty
f =<< empty -- LHS
f =<< absurd <$> nil ()
f . absurd =<< nil ()
-- By parametricity, f . absurd = absurd, for any f.
absurd =<< nil ()
return . absurd =<< nil ()
absurd <$> nil ()
empty -- LHS = RHS
Intuitivamente, si empty
está realmente vacío (como debe ser, dada la definición que estamos usando aquí), no habrá valores para f
aplicar, por f =<< empty
lo que no puede resultar en nada más que empty
.
Un enfoque diferente aquí sería investigar la interacción de las clases Alternative
y Monad
. Da la casualidad que hay una clase de mónadas alternativas: MonadPlus
. En consecuencia, un rediseñado mapMaybe
podría verse así:
-- Lawful iff, for any f, mzero >>= maybe empty mzero . f = mzero
mmapMaybe :: MonadPlus m => (a -> Maybe b) -> m a -> m b
mmapMaybe f m = m >>= maybe mzero return . f
Si bien existen diversas opiniones sobre qué conjunto de leyes es el más apropiado MonadPlus
, una de las leyes que nadie parece objetar es ...
mzero >>= f = mzero -- Left zero
... que es precisamente la propiedad de la empty
que estábamos discutiendo algunos párrafos anteriores. La legalidad de mmapMaybe
sigue inmediatamente de la ley cero izquierda.
(Por cierto, Control.Monad
proporcionamfilter :: MonadPlus m => (a -> Bool) -> m a -> m a
, que coincide con el filter
que podemos definir usando mmapMaybe
).
En resumen:
Pero, ¿es esta implementación siempre legal? ¿A veces es legal (para alguna definición formal de "a veces")?
Sí, la implementación es legal. Esta conclusión depende del empty
hecho de que esté vacío, como debería, o de la mónada alternativa relevante que sigue la MonadPlus
ley de cero a la izquierda , que se reduce a casi lo mismo.
Vale la pena enfatizar que Filterable
no está subsumido MonadPlus
, como podemos ilustrar con los siguientes contraejemplos:
ZipList
: filtrable, pero no una mónada. La Filterable
instancia es la misma que para las listas, a pesar de Alternative
que es diferente.
Map
: filtrable, pero ni mónada ni aplicativo. De hecho, Map
ni siquiera puede ser aplicativo porque no hay una implementación sensata de pure
. Sin embargo, tiene el suyo empty
.
MaybeT f
: mientras que sus instancias Monad
y deben ser una mónada, y una definición aislada necesitaría al menos , la instancia solo requiere (cualquier cosa se puede filtrar si desliza una capa en ella).Alternative
f
empty
Applicative
Filterable
Functor f
Maybe
Tercera parte: vacía
En este punto, uno todavía podría preguntarse qué tan importante es un papel empty
o qué nil
realmente juega Filterable
. No es un método de clase y, sin embargo, la mayoría de las instancias parecen tener una versión sensata del mismo.
De lo único que podemos estar seguros es que, si el tipo filtrable tiene habitantes, al menos uno de ellos será una estructura vacía, porque siempre podemos tomar a cualquier habitante y filtrar todo:
chop :: Filterable f => f a -> f Void
chop = mapMaybe (const Nothing)
La existencia de chop
, sin embargo, no significa que habrá un solo nil
valor vacío, o que chop
siempre dará el mismo resultado. Consideremos, por ejemplo, MaybeT IO
cuyo Filterable
ejemplo podría ser considerado como una forma de censurar los resultados de IO
los cálculos. La instancia es perfectamente legal, aunque chop
puede producir MaybeT IO Void
valores distintos que conllevan IO
efectos arbitrarios .
En una nota final, usted ha aludido a la posibilidad de trabajar con fuertes funtores monoidales, por lo que Alternative
y Filterable
están vinculados al hacer union
/ partition
y nil
/ trivial
isomorfismos. Tener union
y partition
como inversos mutuos es concebible pero bastante limitante, dado que union . partition
descarta cierta información sobre la disposición de los elementos para una gran parte de las instancias. En cuanto al otro isomorfismo, trivial . nil
es trivial, pero nil . trivial
es interesante porque implica que solo hay un f Void
valor único , algo que vale para una parte considerable de Filterable
instancias. Sucede que hay una MonadPlus
versión de esta condición. Si exigimos eso, para cualquier u
...
absurd <$> chop u = mzero
... y luego sustituimos la mmapMaybe
parte dos, obtenemos:
absurd <$> chop u = mzero
absurd <$> mmapMaybe (const Nothing) u = mzero
mmapMaybe (fmap absurd . const Nothing) u = mzero
mmapMaybe (const Nothing) u = mzero
u >>= maybe mzero return . const Nothing = mzero
u >>= const mzero = mzero
u >> mzero = mzero
Esta propiedad se conoce como la ley de cero a la derecha MonadPlus
, aunque hay buenas razones para impugnar su condición de ley de esa clase en particular.