Aqui vai um argumento que apoia amplamente sua bela ideia.
Parte um: mapMaybe
Meu plano aqui é reafirmar o problema em termos de mapMaybe
, esperando que isso nos leve a um terreno mais familiar. Para fazer isso, usarei algumas Either
funções do utilitário -juggling:
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
(Peguei os três primeiros nomes de relude e o quarto de erros . A propósito, os erros oferecem maybeToRight
e rightToMaybe
como note
e hush
respectivamente, em Control.Error.Util
.)
Como você observou, mapMaybe
pode ser definido em termos de partition
:
mapMaybe :: Filterable f => (a -> Maybe b) -> f a -> f b
mapMaybe f = snd . partition . fmap (maybeToRight () . f)
Fundamentalmente, também podemos fazer o contrário:
partition :: Filterable f => f (Either a b) -> (f a, f b)
partition = mapMaybe leftToMaybe &&& mapMaybe rightToMaybe
Isso sugere que faz sentido reformular suas leis em termos de mapMaybe
. Com as leis de identidade, isso nos dá uma ótima desculpa para esquecer completamente trivial
:
-- Left and right unit
mapMaybe rightToMaybe . fmap (bwd elunit) = id -- [I]
mapMaybe leftToMaybe . fmap (bwd erunit) = id -- [II]
Quanto à associatividade, podemos usar rightToMaybe
e leftToMaybe
dividir a lei em três equações, uma para cada componente que obtemos das partições sucessivas:
-- 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]
Parametridade significa mapMaybe
é agnóstico em relação aos Either
valores com os quais estamos lidando aqui. Sendo assim, podemos usar nosso pequeno arsenal de Either
isomorfismos para embaralhar as coisas e mostrar que [I] é equivalente a [II] e [III] é equivalente a [V]. Agora, temos três equações:
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]
A parametridade nos permite engolir o fmap
in [I]:
mapMaybe (rightToMaybe . bwd elunit) = id
Isso, no entanto, é simplesmente ...
mapMaybe Just = id
... o que equivale à lei de conservação / identidade da da witherable 'sFilterable
:
mapMaybe (Just . f) = fmap f
Que Filterable
também tem uma lei de composição:
-- The (<=<) is from the Maybe monad.
mapMaybe g . mapMaybe f = mapMaybe (g <=< f)
Também podemos derivar este de nossas leis? Vamos começar de [III] e, mais uma vez, a parametridade faz seu trabalho. Este é mais complicado, então vou anotá-lo na íntegra:
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
Na outra direção:
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
(Observação: enquanto maybeToRight () . rightToMaybe :: Either a b -> Either () b
não estiver id
, na derivação acima os valores à esquerda serão descartados de qualquer maneira, portanto, é justo eliminá-lo como se fosse id
.)
Assim [III] é equivalente à lei composição de witherable s' Filterable
.
Neste ponto, podemos usar a lei de composição para lidar com [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.
Basta mostrar que sua classe equivale a uma formulação bem estabelecida de Filterable
, o que é um resultado muito bom. Aqui está um resumo das leis:
mapMaybe Just = id -- Identity
mapMaybe g . mapMaybe f = mapMaybe (g <=< f) -- Composition
Como observam os documentos ocultáveis , essas são leis de um functor de Kleisli Maybe a Hask .
Parte dois: Alternativa e Mônada
Agora podemos responder à sua pergunta real, que era sobre mônadas alternativas. Sua implementação proposta partition
foi:
partitionAM :: (Alternative f, Monad f) => f (Either a b) -> (f a, f b)
partitionAM
= (either return (const empty) =<<) &&& (either (const empty) return =<<)
Seguindo meu plano mais amplo, mudarei para a mapMaybe
apresentação:
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 =<<)
E assim podemos definir:
mapMaybeAM :: (Alternative f, Monad f) => (a -> Maybe b) -> f a -> f b
mapMaybeAM f u = maybe empty return . f =<< u
Ou, em uma ortografia sem ponto:
mapMaybeAM = (=<<) . (maybe empty return .)
Alguns parágrafos acima, observei que as Filterable
leis dizem que mapMaybe
é o mapeamento morfístico de um functor de Kleisli Maybe a Hask . Dado que a composição de functors é um functor, e (=<<)
é o mapeamento morphism de um functor de Kleisli f a Hask , (maybe empty return .)
sendo o mapeamento morphism de um functor de Kleisli Talvez a Kleisli f suficiente para mapMaybeAM
ser legal. As leis relevantes do functor são:
maybe empty return . Just = return -- Identity
maybe empty return . g <=< maybe empty return . f
= maybe empty return . (g <=< f) -- Composition
Essa lei de identidade é válida, então vamos nos concentrar na composição:
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.
Portanto, mapMaybeAM
é lícito se maybe empty return . g =<< empty = empty
for o caso g
. Agora, se empty
for definido como absurd <$> nil ()
, como você fez aqui, podemos provar que, f =<< empty = empty
para qualquer 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, se empty
estiver realmente vazio (como deve ser, dada a definição que estamos usando aqui), não haverá valores f
a serem aplicados e, portanto, f =<< empty
não poderá resultar em nada além deempty
.
Uma abordagem diferente aqui seria analisar a interação das classes Alternative
e Monad
. Quando isso acontece, há uma classe para monads alternativas: MonadPlus
. Por conseguinte, um reestilizado mapMaybe
pode ser assim:
-- 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
Embora existam opiniões variadas sobre qual conjunto de leis é mais apropriado MonadPlus
, uma das leis para as quais ninguém parece se opor é ...
mzero >>= f = mzero -- Left zero
... que é precisamente a propriedade de empty
discutirmos alguns parágrafos acima. A legalidade demmapMaybe
segue imediatamente a partir da lei zero esquerda.
(Aliás, Control.Monad
fornecemfilter :: MonadPlus m => (a -> Bool) -> m a -> m a
, que corresponde ao filter
que podemos definir usandommapMaybe
.)
Em suma:
Mas essa implementação é sempre legal? Às vezes é legal (para alguma definição formal de "às vezes")?
Sim, a implementação é legal. Esta conclusão depende do empty
fato de estar vazio, como deveria, ou da mônada alternativa relevante após o zero à esquerdaMonadPlus
lei , que se resume a praticamente a mesma coisa.
Vale ressaltar que Filterable
não é subsumido por MonadPlus
, como podemos ilustrar com os seguintes contra-exemplos:
ZipList
: filtrável, mas não uma mônada. A Filterable
instância é igual à das listas, mesmo que Alternative
diferente.
Map
: filtrável, mas nem mônada nem aplicativo. De fato, Map
nem sequer pode ser aplicável porque não há uma implementação sensata do pure
. No entanto, tem o seu próprio empty
.
MaybeT f
: enquanto suas instâncias Monad
e Alternative
precisam f
ser uma mônada, e uma empty
definição isolada precisaria de pelo menos Applicative
, a Filterable
instância requer apenas Functor f
(tudo se torna filtrável se você Maybe
inserir uma camada nela).
Parte três: vazia
Nesse ponto, ainda podemos nos perguntar o tamanho de um papel empty
, ou nil
, realmente desempenha Filterable
. Não é um método de classe e, no entanto, a maioria das instâncias parece ter uma versão sensata dele por aí.
A única coisa que podemos ter certeza é que, se o tipo filtrável tiver habitantes, pelo menos um deles será uma estrutura vazia, porque sempre podemos pegar qualquer habitante e filtrar tudo:
chop :: Filterable f => f a -> f Void
chop = mapMaybe (const Nothing)
A existência de chop
, embora não signifique que haverá um único nil
valor vazio, ou que chop
sempre dará o mesmo resultado. Considere, por exemplo, MaybeT IO
cuja Filterable
instância possa ser pensada como uma maneira de censurar os resultados dos IO
cálculos. A instância é perfeitamente lícita, embora chop
possa produzir MaybeT IO Void
valores distintos que carregam IO
efeitos arbitrários .
Em uma nota final, de ter aludido a possibilidade de trabalhar com fortes functors monoidais, de modo que Alternative
e Filterable
estão ligados por fazer union
/ partition
e nil
/ trivial
isomorfismos. Ter union
e partition
como inverso mútuo é concebível, mas bastante limitador, dado que union . partition
descarta algumas informações sobre o arranjo dos elementos para uma grande parcela de instâncias. Quanto ao outro isomorfismo, trivial . nil
é trivial, mas nil . trivial
é interessante, pois implica que há apenas um único f Void
valor, algo que vale para uma parcela considerável de Filterable
instâncias. Acontece que existe uma MonadPlus
versão dessa condição. Se exigirmos isso, para qualquer u
...
absurd <$> chop u = mzero
... e, em seguida, substitua o mmapMaybe
da parte dois, obtemos:
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
Essa propriedade é conhecida como lei zero correta MonadPlus
, embora haja boas razões para contestar seu status como uma lei dessa classe específica.