Hier ein Argument, das Ihre schöne Idee weitgehend unterstützt.
Teil eins: mapMaybe
Mein Plan hier ist es, das Problem in Bezug auf neu zu formulieren, in der mapMaybeHoffnung, dass dies uns zu einem vertrauten Boden bringt. Zu diesem EitherZweck werde ich einige Dienstprogramme zum Jonglieren verwenden:
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
(Ich habe die ersten drei Namen von relude und die vierte von Fehlern . Übrigens, Fehler Angebote maybeToRightund rightToMaybewie noteund hushjeweils in Control.Error.Util.)
Wie Sie bemerkt haben, mapMaybekann definiert werden in Bezug auf partition:
mapMaybe :: Filterable f => (a -> Maybe b) -> f a -> f b
mapMaybe f = snd . partition . fmap (maybeToRight () . f)
Entscheidend ist, dass wir auch umgekehrt vorgehen können:
partition :: Filterable f => f (Either a b) -> (f a, f b)
partition = mapMaybe leftToMaybe &&& mapMaybe rightToMaybe
Dies legt nahe, dass es sinnvoll ist, Ihre Gesetze in Bezug auf neu zu formulieren mapMaybe. Mit den Identitätsgesetzen gibt uns dies eine gute Entschuldigung, um ganz zu vergessen trivial:
-- Left and right unit
mapMaybe rightToMaybe . fmap (bwd elunit) = id -- [I]
mapMaybe leftToMaybe . fmap (bwd erunit) = id -- [II]
Was die Assoziativität betrifft, können wir das Gesetz verwenden rightToMaybeund leftToMaybein drei Gleichungen aufteilen, eine für jede Komponente, die wir aus den aufeinanderfolgenden Partitionen erhalten:
-- 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]
Parametrizitätsmittel mapMaybesind in Bezug auf die EitherWerte, mit denen wir uns hier befassen, agnostisch . Unter diesen Umständen können wir unser kleines Arsenal an EitherIsomorphismen verwenden, um Dinge zu mischen und zu zeigen, dass [I] gleich [II] und [III] gleich [V] ist. Wir haben jetzt drei Gleichungen:
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]
Die Parametrizität ermöglicht es uns, das fmapin [I] zu schlucken :
mapMaybe (rightToMaybe . bwd elunit) = id
Das ist jedoch einfach ...
mapMaybe Just = id
... , die zur Erhaltung / Identität Gesetz von entspricht witherable ‚sFilterable :
mapMaybe (Just . f) = fmap f
Das Filterablehat auch ein Kompositionsgesetz:
-- The (<=<) is from the Maybe monad.
mapMaybe g . mapMaybe f = mapMaybe (g <=< f)
Können wir dies auch aus unseren Gesetzen ableiten? Beginnen wir mit [III] und lassen wir noch einmal die Parametrizität ihre Arbeit machen. Dieser ist kniffliger, also schreibe ich ihn vollständig auf:
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
In die andere Richtung:
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
(Hinweis: Während dies maybeToRight () . rightToMaybe :: Either a b -> Either () bnicht idder Fall ist , werden in der obigen Ableitung die linken Werte ohnehin verworfen. Es ist also fair, sie so zu streichen, als ob sie es wären id.)
Somit [III] ist äquivalent zu der Zusammensetzung Gesetz witherable ist Filterable.
An dieser Stelle können wir das Kompositionsgesetz verwenden, um mit [IV] umzugehen:
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.
Dies reicht aus, um zu zeigen, dass Ihre Klassenmengen eine gut etablierte Formulierung von sind Filterable, was ein sehr schönes Ergebnis ist. Hier ist eine Zusammenfassung der Gesetze:
mapMaybe Just = id -- Identity
mapMaybe g . mapMaybe f = mapMaybe (g <=< f) -- Composition
Wie die verwelkten Dokumente vermerken, sind dies Funktorgesetze für einen Funktor von Kleisli Vielleicht bis Hask .
Teil zwei: Alternative und Monade
Jetzt können wir Ihre eigentliche Frage beantworten, die sich mit alternativen Monaden befasste. Ihre vorgeschlagene Implementierung von partitionwar:
partitionAM :: (Alternative f, Monad f) => f (Either a b) -> (f a, f b)
partitionAM
= (either return (const empty) =<<) &&& (either (const empty) return =<<)
Nach meinem breiteren Plan werde ich zur mapMaybePräsentation wechseln :
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 =<<)
Und so können wir definieren:
mapMaybeAM :: (Alternative f, Monad f) => (a -> Maybe b) -> f a -> f b
mapMaybeAM f u = maybe empty return . f =<< u
Oder in einer punktfreien Schreibweise:
mapMaybeAM = (=<<) . (maybe empty return .)
Ein paar Absätze oben habe ich bemerkt Filterable, dass die Gesetze besagen, dass dies mapMaybedie Morphismus-Zuordnung eines Funktors von Kleisli Vielleicht zu Hask ist . Da die Zusammensetzung von functors ist ein Funktors, und (=<<)ist die morphism Abbildung eines Funktors von Kleisli f zu Hask , (maybe empty return .)die morphism Abbildung eines Funktors entfernt, Vielleicht Kleisli bis f Kleisli reicht für mapMaybeAMrechtmäßig. Die relevanten Funktorgesetze sind:
maybe empty return . Just = return -- Identity
maybe empty return . g <=< maybe empty return . f
= maybe empty return . (g <=< f) -- Composition
Dieses Identitätsgesetz gilt, also konzentrieren wir uns auf die Komposition:
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.
Daher mapMaybeAMist rechtmäßig, wenn maybe empty return . g =<< empty = emptyfür jeden g. Wenn nun emptydefiniert ist absurd <$> nil (), wie Sie es hier getan haben, können wir dies f =<< empty = emptyfür jeden beweisen 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
Wenn emptyes wirklich leer ist (wie es sein muss, angesichts der Definition, die wir hier verwenden), gibt es intuitiv keine Werte, fauf die angewendet werden kann, und f =<< emptykann daher nur zu etwas führen empty.
Ein anderer Ansatz wäre hier die Untersuchung der Interaktion von Alternativeund MonadKlassen. Zufällig gibt es eine Klasse für alternative Monaden : MonadPlus. Dementsprechend mapMaybekönnte ein neu gestaltetes so aussehen:
-- 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
Zwar gibt es unterschiedliche Meinungen darüber, für welche Gesetze am besten geeignet ist MonadPlus, aber eines der Gesetze, gegen die niemand Einwände zu erheben scheint, ist ...
mzero >>= f = mzero -- Left zero
... was genau die Eigenschaft ist, über die emptywir oben einige Absätze besprochen haben. Die Rechtmäßigkeit von mmapMaybefolgt unmittelbar aus dem linken Nullgesetz.
(Übrigens Control.Monadliefertmfilter :: MonadPlus m => (a -> Bool) -> m a -> m a , was mit dem übereinstimmt, was filterwir mit definieren können mmapMaybe.)
Zusammenfassend:
Aber ist diese Implementierung immer rechtmäßig? Ist es manchmal rechtmäßig (für eine formale Definition von "manchmal")?
Ja, die Implementierung ist rechtmäßig. Diese Schlussfolgerung hängt davon ab empty, ob es tatsächlich leer ist, wie es sollte, oder von der relevanten alternativen Monade, die dem MonadPlusGesetz des linken Nullpunkts folgt , was auf fast dasselbe hinausläuft.
Hervorzuheben ist, dass dies Filterablenicht subsumiert wird MonadPlus, wie wir anhand der folgenden Gegenbeispiele veranschaulichen können:
ZipList: filterbar, aber keine Monade. Die FilterableInstanz ist dieselbe wie die für Listen, auch wenn die Alternativeandere unterschiedlich ist.
Map: filtrierbar, aber weder monad noch anwendbar. In der Tat Mapkann nicht einmal anwendbar sein, weil es keine vernünftige Implementierung von gibt pure. Es hat jedoch seine eigenen empty.
MaybeT f: Während seine Monadund AlternativeInstanzen feine Monade sein müssen und eine isolierte emptyDefinition zumindest eine Instanz erfordern würde Applicative, Filterableerfordert die Instanz nur Functor f(alles wird filterbar, wenn Sie eine MaybeEbene hineinschieben).
Teil drei: leer
An diesem Punkt könnte man sich noch fragen, wie groß die Rolle emptyist oder nilwirklich spielt Filterable. Es ist keine Klassenmethode, und dennoch scheinen die meisten Instanzen eine vernünftige Version davon zu haben.
Das eine, dessen wir uns sicher sein können, ist, dass, wenn der filterbare Typ überhaupt Einwohner hat, mindestens einer von ihnen eine leere Struktur ist, weil wir immer jeden Einwohner nehmen und alles herausfiltern können:
chop :: Filterable f => f a -> f Void
chop = mapMaybe (const Nothing)
Die Existenz von chopbedeutet jedoch nicht, dass es einen einzelnen nil leeren Wert gibt, oder dass dies chopimmer das gleiche Ergebnis ergibt. Stellen Sie sich zum Beispiel vor, MaybeT IOwessen FilterableInstanz als eine Möglichkeit angesehen werden könnte, Ergebnisse von IOBerechnungen zu zensieren . Die Instanz ist vollkommen rechtmäßig, obwohl chopsie unterschiedliche MaybeT IO VoidWerte erzeugen kann , die willkürliche IOAuswirkungen haben.
Abschließend haben Sie auf die Möglichkeit hingewiesen, mit starken monoidalen Funktoren zu arbeiten, so dass Alternativeund Filterabledurch Herstellung von union/ partitionund nil/ trivialIsomorphismen verbunden sind. Das Haben unionund partitionals gegenseitige Umkehrung ist denkbar, aber ziemlich einschränkend, da union . partitioneinige Informationen über die Anordnung der Elemente für einen großen Teil der Instanzen verworfen werden. Der andere Isomorphismus trivial . nilist trivial, aber nil . trivialinsofern interessant, als er impliziert, dass es nur einen einzigen f VoidWert gibt, der für einen beträchtlichen Anteil von FilterableInstanzen gilt. Es kommt vor, dass es eine MonadPlusVersion dieser Bedingung gibt. Wenn wir das verlangen, für jeden u...
absurd <$> chop u = mzero
... und dann ersetzen Sie die mmapMaybe aus Teil zwei, wir bekommen:
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
Diese Eigenschaft ist als das Recht auf Recht Null bekannt MonadPlus, obwohl es gute Gründe gibt, ihren Status als Gesetz dieser bestimmten Klasse anzufechten.