Ist jede alternative Monade filterbar?

8

Die Kategorie der Mengen ist sowohl kartesisch monoidal als auch kokartesisch monoidal. Die Arten der kanonischen Isomorphismen, die diese beiden monoidalen Strukturen bezeugen, sind nachstehend aufgeführt:

type x + y = Either x y
type x × y = (x, y)

data Iso a b = Iso { fwd :: a -> b, bwd :: b -> a }

eassoc :: Iso ((x + y) + z) (x + (y + z))
elunit :: Iso (Void + x) x
erunit :: Iso (x + Void) x

tassoc :: Iso ((x × y) × z) (x × (y × z))
tlunit :: Iso (() × x) x
trunit :: Iso (x × ()) x

Für die Zwecke dieser Frage definiere Alternativeich einen laxen monoidalen Funktor von Hask unter dem EitherTensor zu Hask unter dem (,)Tensor (und nicht mehr):

class Functor f => Alt f
  where
  union :: f a × f b -> f (a + b)

class Alt f => Alternative f
  where
  nil :: () -> f Void

Die Gesetze sind nur die für einen laxen monoidalen Funktor.

Assoziativität:

fwd tassoc >>> bimap id union >>> union
=
bimap union id >>> union >>> fmap (fwd eassoc)

Linke Einheit:

fwd tlunit
=
bimap nil id >>> union >>> fmap (fwd elunit)

Rechte Einheit:

fwd trunit
=
bimap id nil >>> union >>> fmap (fwd erunit)

Hier Alternativeerfahren Sie, wie Sie die bekannteren Operationen für die Typklasse in Bezug auf die Kohärenzkarten der laxen monoidalen Funktorkodierung wiederherstellen können:

(<|>) :: Alt f => f a -> f a -> f a
x <|> y = either id id <$> union (Left <$> x, Right <$> y)

empty :: Alternative f => f a
empty = absurd <$> nil ()

Ich definiere FilterableFunktoren als Oplax-Monoid- Funktoren von Hask unter dem EitherTensor bis Hask unter dem (,)Tensor:

class Functor f => Filter f
  where
  partition :: f (a + b) -> f a × f b

class Filter f => Filterable f
  where
  trivial :: f Void -> ()
  trivial = const ()

Für seine Gesetze nur rückwärts laxe monoidale Funktorgesetze zu haben:

Assoziativität:

bwd tassoc <<< bimap id partition <<< partition
=
bimap partition id <<< partition <<< fmap (bwd eassoc)

Linke Einheit:

bwd tlunit
=
bimap trivial id <<< partition <<< fmap (bwd elunit)

Rechte Einheit:

bwd trunit
=
bimap id trivial <<< partition <<< fmap (bwd erunit)

Definieren von Standardfilter-y-Funktionen wie mapMaybeund filterin Bezug auf die oplaxe monoidale Funktorkodierung, die dem interessierten Leser als Übung überlassen werden:

mapMaybe :: Filterable f => (a -> Maybe b) -> f a -> f b
mapMaybe = _

filter :: Filterable f => (a -> Bool) -> f a -> f a
filter = _

Die Frage ist: Ist jeder Alternative Monadauch Filterable?

Wir können tetris auf dem Weg zu einer Implementierung eingeben:

instance (Alternative f, Monad f) => Filter f
  where
  partition fab = (fab >>= either return (const empty), fab >>= either (const empty) return)

Aber ist diese Implementierung immer rechtmäßig? Ist es manchmal rechtmäßig (für eine formale Definition von "manchmal")? Beweise, Gegenbeispiele und / oder informelle Argumente wären alle sehr nützlich. Vielen Dank.

Asad Saeeduddin
quelle
Persönlich wäre ich ziemlich überrascht, wenn es immer gültig wäre. Es ist sicherlich manchmal in dem Sinne gültig, dass es Funktoren gibt, für die es gültig ist, aber ich bezweifle, dass dies eine besonders interessante Klasse ist.
dfeuer
@dfeuer Haben Sie Gegenbeispiele im Sinn, für die es offensichtlich nicht gültig ist? Vielleicht wäre das lehrreich.
Asad Saeeduddin
Ich bin mir ziemlich sicher, dass dies immer eine rechtmäßige Instanz ist, nur weil die Definitionen entfaltet und trivial umgeschrieben wurden (was darauf hindeutet, dass die FilterableGesetze ziemlich schwach sind). @AsadSaeeduddin Erwägen Sie, einige interaktive Theorem-Beweisfähigkeiten zu erlernen, damit Sie die Mentalität "Verwendungstypen, nicht Ihr Gehirn" auch auf Beweise ausweiten können!
Li-yao Xia

Antworten:

3

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.

Duplode
quelle
Vielen Dank, dass Sie sich die Zeit genommen haben, dies aufzuschreiben! Ich hatte noch keine Zeit, dies alles durchzugehen, aber es ist fair zu sagen, dass die Zusammenfassung lautet: "Nicht ohne zusätzliche Gesetze in Bezug auf die Monadund AlternativeInstanzen"?
Asad Saeeduddin
@ AsadSaeeduddin Yup. MonadPlus(möglicherweise durch die nahezu semirische Charakterisierung gesehen ) stellt eine Verbindung zwischen Alternativeund Monadder schmucklosen "monoidalen Funktor von Hask-mit- (,)zu-Hask-mit- Either" Charakterisierung nicht her. Auf jeden Fall frage ich mich immer noch, ob hier etwas Tieferes auftaucht empty, das der bloßen FilterableDefinition fremd erscheint und sich dennoch recht passend anfühlt.
Duplode
emptyist Teil der Definition von Alternative. Das Alternativeund Filterablekann enger miteinander verbunden werden, indem verlangt wird, dass uniondas Gegenteil von partition(und umgekehrt) und trivialdas Gegenteil von nil(und umgekehrt) ist. Dies wird als "starker monoidaler Funktor" bezeichnet.
Asad Saeeduddin
@AsadSaeeduddin Für einige der häufigsten FilterableFälle wäre diese Eigenschaft zu stark, da partitionsie verlustbehaftet sein kann. Zum Beispiel (union . partition) [L 7, R 2, L 1, R 6]ist [L 7, L 1, R 2, R 6]. Der trivial/ nilTeil würde letztendlich darauf hinauslaufen, nur einen f VoidWert zu haben, was zugänglicher erscheint. In MonadPlusBegriffen entspricht es dem umstrittenen Recht-Null-Gesetz, m >> mzero = mzerodas beispielsweise für Listen gilt, nicht jedoch für Parser.
Duplode
1
@AsadSaeeduddin Ich habe einen Abschnitt angehängt, in dem meine Gedanken zusammengefasst sind empty. Dies ist möglicherweise das letzte Update :)
Duplode