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 mapMaybe
Hoffnung, dass dies uns zu einem vertrauten Boden bringt. Zu diesem Either
Zweck 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 maybeToRight
und rightToMaybe
wie note
und hush
jeweils in Control.Error.Util
.)
Wie Sie bemerkt haben, mapMaybe
kann 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 rightToMaybe
und leftToMaybe
in 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 mapMaybe
sind in Bezug auf die Either
Werte, mit denen wir uns hier befassen, agnostisch . Unter diesen Umständen können wir unser kleines Arsenal an Either
Isomorphismen 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 fmap
in [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 Filterable
hat 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 () b
nicht id
der 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 partition
war:
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 mapMaybe
Prä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 mapMaybe
die 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 mapMaybeAM
rechtmäß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 mapMaybeAM
ist rechtmäßig, wenn maybe empty return . g =<< empty = empty
für jeden g
. Wenn nun empty
definiert ist absurd <$> nil ()
, wie Sie es hier getan haben, können wir dies f =<< empty = empty
fü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 empty
es wirklich leer ist (wie es sein muss, angesichts der Definition, die wir hier verwenden), gibt es intuitiv keine Werte, f
auf die angewendet werden kann, und f =<< empty
kann daher nur zu etwas führen empty
.
Ein anderer Ansatz wäre hier die Untersuchung der Interaktion von Alternative
und Monad
Klassen. Zufällig gibt es eine Klasse für alternative Monaden : MonadPlus
. Dementsprechend mapMaybe
kö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 empty
wir oben einige Absätze besprochen haben. Die Rechtmäßigkeit von mmapMaybe
folgt unmittelbar aus dem linken Nullgesetz.
(Übrigens Control.Monad
liefertmfilter :: MonadPlus m => (a -> Bool) -> m a -> m a
, was mit dem übereinstimmt, was filter
wir 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 MonadPlus
Gesetz des linken Nullpunkts folgt , was auf fast dasselbe hinausläuft.
Hervorzuheben ist, dass dies Filterable
nicht subsumiert wird MonadPlus
, wie wir anhand der folgenden Gegenbeispiele veranschaulichen können:
ZipList
: filterbar, aber keine Monade. Die Filterable
Instanz ist dieselbe wie die für Listen, auch wenn die Alternative
andere unterschiedlich ist.
Map
: filtrierbar, aber weder monad noch anwendbar. In der Tat Map
kann nicht einmal anwendbar sein, weil es keine vernünftige Implementierung von gibt pure
. Es hat jedoch seine eigenen empty
.
MaybeT f
: Während seine Monad
und Alternative
Instanzen f
eine Monade sein müssen und eine isolierte empty
Definition zumindest eine Instanz erfordern würde Applicative
, Filterable
erfordert die Instanz nur Functor f
(alles wird filterbar, wenn Sie eine Maybe
Ebene hineinschieben).
Teil drei: leer
An diesem Punkt könnte man sich noch fragen, wie groß die Rolle empty
ist oder nil
wirklich 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 chop
bedeutet jedoch nicht, dass es einen einzelnen nil
leeren Wert gibt, oder dass dies chop
immer das gleiche Ergebnis ergibt. Stellen Sie sich zum Beispiel vor, MaybeT IO
wessen Filterable
Instanz als eine Möglichkeit angesehen werden könnte, Ergebnisse von IO
Berechnungen zu zensieren . Die Instanz ist vollkommen rechtmäßig, obwohl chop
sie unterschiedliche MaybeT IO Void
Werte erzeugen kann , die willkürliche IO
Auswirkungen haben.
Abschließend haben Sie auf die Möglichkeit hingewiesen, mit starken monoidalen Funktoren zu arbeiten, so dass Alternative
und Filterable
durch Herstellung von union
/ partition
und nil
/ trivial
Isomorphismen verbunden sind. Das Haben union
und partition
als gegenseitige Umkehrung ist denkbar, aber ziemlich einschränkend, da union . partition
einige Informationen über die Anordnung der Elemente für einen großen Teil der Instanzen verworfen werden. Der andere Isomorphismus trivial . nil
ist trivial, aber nil . trivial
insofern interessant, als er impliziert, dass es nur einen einzigen f Void
Wert gibt, der für einen beträchtlichen Anteil von Filterable
Instanzen gilt. Es kommt vor, dass es eine MonadPlus
Version 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.