Gibt es eine Haskell-Sprache, um mehrere Funktionen auszuprobieren und zu stoppen, sobald eine erfolgreich ist?

9

In Haskell kann ich den Typ verwenden a -> Maybe b, um eine Funktion zu modellieren, die entweder einen Wert vom Typ bzurückgibt oder nichts zurückgibt (dies schlägt fehl).

Wenn ich Typen a1, ..., a(n+1)und Funktionen f1, ..., fn, mit fi :: ai -> Maybe a(i+1)für alle i, 1 <= i <= nkann ich die Kette , die Funktionen durch die Verwendung des >>=Betreibers der MaybeMonade und schreiben:

f1 x >>= f2 >>= f3 >>=... >>= fn

Der >>=Operator stellt sicher, dass jede Funktion angewendet wird, solange der Vorgänger einen aussagekräftigen Wert zurückgegeben hat. Sobald eine Funktion in der Kette ausfällt, fällt die gesamte Kette aus (kehrt zurück Nothing) und weitere Funktionen in der Kette werden nicht ausgewertet.

Ich habe ein etwas ähnliches Muster, bei dem ich mehrere Funktionen am selben Eingang ausprobieren und zurückkehren möchte, sobald eine Funktion erfolgreich ist . Wenn alle Funktionen fehlschlagen (return Nothing), sollte die gesamte Berechnung fehlschlagen. Genauer gesagt habe ich Funktionen f1, ..., fn :: a -> Maybe bund definiere die Funktion

tryFunctions :: [a -> Maybe b] -> a -> Maybe b
tryFunctions []       _ = Nothing
tryFunctions (f : fs) x = case f x of
                            Nothing    -> tryFunctions fs x
                            r@(Just _) -> r

In gewissem Sinne ist dies für die MaybeMonade insofern dual , als eine Berechnung beim ersten Erfolg statt beim ersten Misserfolg stoppt.

Natürlich kann ich die oben beschriebene Funktion verwenden, aber ich habe mich gefragt, ob es eine bessere, gut etablierte und idiomatische Möglichkeit gibt, dieses Muster in Haskell auszudrücken.

Giorgio
quelle
Nicht Haskell, aber in C # sehen Sie gelegentlich den Null-Coalesce-Operator (??), der so verwendet wird:return f1 ?? f2 ?? f3 ?? DefaultValue;
Telastyn
4
Ja, das ist es - dies ist Alternativeder Symbol-Infix-Operator <|>und wird als Monoid definiert
Jimmy Hoffa

Antworten:

8

Bei einer geschlossenen Menge (feste Anzahl von Elementen) Smit Elementen {a..z}und einem binären Operator *:

Es gibt ein einziges Identitätselement, iso dass:

forall x in S: i * x = x = x * i

Der Operator ist assoziativ, so dass:

forall a, b, c in S: a * (b * c) = (a * b) * c

Du hast ein Monoid.

Wenn Sie nun ein beliebiges Monoid haben, können Sie eine Binärfunktion fwie folgt definieren :

f(i, x) = x
f(x, _) = x

Dies bedeutet, dass für das Beispiel des MaybeMonoids ( Nothingist das oben als bezeichnet bezeichnete Identitätselement i):

f(Nothing, Just 5) = Just 5
f(Just 5, Nothing) = Just 5
f(Just 5, Just 10) = Just 5
f(Nothing, f(Nothing, Just 5)) = Just 5
f(Nothing, f(Just 5, Nothing)) = Just 5

Überraschenderweise kann ich diese genaue Funktion in den Standardbibliotheken nicht finden, was wahrscheinlich auf meine eigene Unerfahrenheit zurückzuführen ist. Wenn jemand anderes dies freiwillig tun kann, würde ich es aufrichtig schätzen.

Hier ist die Implementierung, die ich aus dem obigen Beispiel abgeleitet habe:

(<||>) :: (Monoid a, Eq a) => a -> a -> a
x <||> y
     | x == mempty = y
     | True = x

Beispiel:

λ> [] <||> [1,2] <||> [3,4]
[1,2]
λ> Just "foo" <||> Nothing <||> Just "bar"
Just "foo"
λ> Nothing <||> Just "foo" <||> Just "bar"
Just "foo"
λ> 

Dann, wenn Sie eine Liste von Funktionen als Eingabe verwenden möchten ...

tryFunctions x funcs = foldl1 (<||>) $ map ($ x) funcs

Beispiel:

instance Monoid Bool where
         mempty = False
         mconcat = or
         mappend = (||)

λ> tryFunctions 8 [odd, even]
True
λ> tryFunctions 8 [odd, odd]
False
λ> tryFunctions 8 [odd, odd, even]
True
λ> 
Jimmy Hoffa
quelle
Ich verstehe nicht, warum <|>die Identität eines Monoids auf besondere Weise behandelt wird. Könnte man nicht ein beliebiges Element einer beliebigen Menge auswählen, um diese besondere Rolle zu spielen? Warum muss das Set ein Monoid sein und das besondere Element <|>seiner Identität?
Giorgio
1
@Giorgio vielleicht verlässt man sich deshalb <|>nicht auf Monoid und ich habe das alles durcheinander gebracht? Es basiert auf AlternativeTypklasse. Um sicher zu sein - ich schaue auf meine eigene Antwort und stelle fest, dass sie nicht ganz richtig ist, da sie [1,2] <|> [3]das Unerwartete ergibt, [1,2,3]sodass alles an der Verwendung der Monoidtypklasse zur Identifizierung einer Identität richtig ist - und der andere Schlüssel ist die Assoziativität, die erforderlich ist, um das erwartete Verhalten zu erhalten , gibt vielleicht Alternativenicht das Verhalten, das ich aus der Hand gedacht habe ...
Jimmy Hoffa
@JimmyHoffa es wird von der Typklasse abhängen Vielleicht unterscheidet sich <|> von List <|> nein?
jk.
@Giorgio schau dir meine letzten 2 Beispiele an, um fzu sehen, warum Assoziativität eine Notwendigkeit ist.
Jimmy Hoffa
dh Monoid für Listen ist die leere Liste und concat
jk.
5
import Data.Monoid

tryFunctions :: a -> [a -> Maybe b] -> Maybe b
tryFunctions x = getFirst . mconcat . map (First . ($ x))
Thomas Eding
quelle
Dies ist sauber und einfach, aber behoben auf Maybe... Meine Lösung ist eingeschränkt durch Eq, irgendwie habe ich das Gefühl, dass wir beide etwas vermissen, das Monoidallgemein verfügbar ist ...
Jimmy Hoffa
@ JimmyHoffa: Meinst du, du möchtest diese Lösung verallgemeinern, damit sie mit anderen Datentypen funktioniert, z either.
Giorgio
@ Giorgio genau. Ich wünschte, die einzige Einschränkung könnte sein Monoid, dass alles mit einem Identitätselement einen Satz von 2 Funktionen haben könnte (ich denke, dies ist eine Art Binärfeld), wobei eine der Funktionen die Identität über alle anderen wählt und die andere Funktion immer wählt das Nichtidentitätselement. Ich weiß nur nicht, wie ich das machen soll, ohne Eqzu wissen, welcher Wert ist identityoder nicht. Offensichtlich erhalten Sie in additiven oder multiplikativen Monoiden standardmäßig die Kontaktplanfunktion (wählen Sie immer Nichtidentitätselemente mit der Monoid-Binärfunktion)
Jimmy Hoffa
foldMapkann anstelle vonmconcat . map
4castle
4

Das klingt sehr danach , Fehler durch eine Liste von Erfolgen zu ersetzen

Sie sprechen Maybe aeher über als [a], aber in der Tat sind sie sich sehr ähnlich: Wir können uns vorstellen, dass es Maybe aso ist [a], außer dass es höchstens ein Element enthalten kann (dh Nothing ~= []und Just x ~= [x]).

Bei Listen tryFunctionswäre dies sehr einfach: Wenden Sie alle Funktionen auf das angegebene Argument an und verketten Sie dann alle Ergebnisse miteinander. concatMapwird das schön machen:

tryFunctions :: [a -> [b]] -> a -> [b]
tryFunctions fs x = concatMap ($ x) fs

Auf diese Weise können wir sehen, dass der <|>Operator für Maybewie eine Verkettung für 'Listen mit höchstens einem Element' wirkt.

Warbo
quelle
1

Teilen Sie es im Geiste von Conal in kleinere, einfachere Operationen auf.

In diesem Fall asumaus Data.Foldablemacht den Hauptteil.

tryFunction fs x = asum (map ($ x) fs)

Alternativ können Sie die MonoidInstanz von a la Jimmy Hoffa für verwenden, (->)aber dann benötigen Sie eine MonoidInstanz für Maybeund die Standardinstanz macht nicht das, was Sie wollen. Du willst Firstvon Data.Monoid.

tryFunction = fmap getFirst . fold . map (fmap First)

(Oder mconcatfür eine ältere, spezialisiertere Version von fold.)

Derek Elkins verließ SE
quelle
Interessante Lösungen (+1). Ich finde das erste intuitiver.
Giorgio