¿Existe un modismo de Haskell para probar varias funciones y detenerse tan pronto como una tenga éxito?

9

En Haskell, puedo usar el tipo a -> Maybe bpara modelar una función que devuelve un valor de tipo bo no devuelve nada (falla).

Si tengo tipos a1, ..., a(n+1)y funciones f1, ..., fn, con fi :: ai -> Maybe a(i+1)for all i, 1 <= i <= npuedo encadenar las funciones usando el >>=operador de la Maybemónada y escribir:

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

El >>=operador se asegura de que cada función se aplique siempre que su predecesor haya devuelto un valor significativo. Tan pronto como falla una función en la cadena, la cadena completa falla (retorna Nothing) y no se evalúan otras funciones en la cadena.

Tengo un patrón algo similar en el que quiero probar varias funciones en la misma entrada y regresar tan pronto como una función tenga éxito . Si todas las funciones fallan (retorno Nothing), todo el cálculo debería fallar. Más precisamente, tengo funciones f1, ..., fn :: a -> Maybe by defino la función

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

En cierto sentido, esto es dual para la Maybemónada en que un cálculo se detiene en el primer éxito en lugar de en el primer fracaso.

Por supuesto, puedo usar la función que he escrito anteriormente, pero me preguntaba si hay una forma mejor, bien establecida e idiomática de expresar este patrón en Haskell.

Giorgio
fuente
No Haskell, pero en C #, ocasionalmente verá que el operador de fusión nula (??) se usa así:return f1 ?? f2 ?? f3 ?? DefaultValue;
Telastyn
44
Sí, Alternativees el operador de infijo de símbolo <|>y se define en términos de un monoide
Jimmy Hoffa

Respuestas:

8

Dado un conjunto cerrado (número fijo de elementos) Scon elementos {a..z}y un operador binario *:

Hay un único elemento de identidad ital que:

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

El operador es asociativo de manera que:

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

Tienes un monoide.

Ahora, dado cualquier monoide, puede definir una función binaria fcomo:

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

Lo que esto significa es que, por ejemplo, el Maybemonoide ( Nothinges el elemento de identidad indicado anteriormente como 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

Sorprendentemente, no puedo encontrar esta función precisa en las bibliotecas predeterminadas, lo que probablemente se deba a mi propia inexperiencia. Si alguien más puede ser voluntario en esto, lo agradecería sinceramente.

Aquí está la implementación que deduje de la mano del ejemplo anterior:

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

Ejemplo:

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

Entonces, si desea utilizar una lista de funciones como entrada ...

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

ejemplo:

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
fuente
No entiendo por qué <|>trata la identidad de un monoide de una manera especial. ¿No podría uno elegir un elemento arbitrario de un conjunto arbitrario para desempeñar ese papel especial? ¿Por qué el conjunto tiene que ser un monoide y el elemento especial es <|>su identidad?
Giorgio
1
@Giorgio, tal vez por eso <|>no confía en monoide y tengo todo esto mezclado. Se basa en Alternativetypeclass. Para estar seguro - Estoy mirando mi propia respuesta y darse cuenta de que no está del todo bien como [1,2] <|> [3]da la inesperada [1,2,3]por lo que todo lo relacionado con el uso de la clase de tipo monoid para identificar una identidad que es correcto - y la otra clave es la asociatividad es necesario para obtener el comportamiento esperado , tal vez Alternativeno da el comportamiento que pensé de la mano ...
Jimmy Hoffa
@JimmyHoffa dependerá de la clase típica ¿Quizás <|> será diferente a la Lista <|> no?
jk.
@Giorgio mira mis últimos 2 ejemplos de fpara ver por qué la asociatividad es una necesidad.
Jimmy Hoffa
es decir, monoid para listas es la lista vacía y concat
jk.
5
import Data.Monoid

tryFunctions :: a -> [a -> Maybe b] -> Maybe b
tryFunctions x = getFirst . mconcat . map (First . ($ x))
Thomas Eding
fuente
Esto es limpio y simple pero está arreglado para Maybe... Mi solución está limitada Eq, de alguna manera siento que a los dos nos falta algo disponible en Monoidgeneral ...
Jimmy Hoffa
@JimmyHoffa: ¿Quiere decir que le gustaría generalizar esta solución para que pueda funcionar con otros tipos de datos, por ejemplo either?
Giorgio
@Giorgio exactamente. Desearía que la única restricción pudiera ser Monoidque cualquier cosa con un elemento de identidad pueda tener un conjunto de 2 funciones (creo que este es un tipo de campo binario), donde una de las funciones elige identidad sobre todas las demás, y la otra función siempre elige El elemento de no identidad. Simplemente no sé cómo hacer esto sin Eqsaber qué valor es identityo no ... obviamente en los monoides aditivos o multiplicativos obtienes la función de escalera por defecto (siempre elige elementos que no sean de identidad usando la función binaria de monoides)
Jimmy Hoffa
foldMapse puede usar en lugar demconcat . map
4castle
4

Esto se parece mucho a reemplazar el fracaso por una lista de éxitos

Estás hablando en Maybe alugar de hacerlo [a], pero en realidad son muy similares: podemos pensar Maybe aque son como [a], excepto que puede contener como máximo un elemento (es decir, Nothing ~= []y Just x ~= [x]).

En el caso de las listas, tryFunctionssería muy simple: aplique todas las funciones al argumento dado y luego concatene todos los resultados juntos. concatMaphará esto muy bien:

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

De esta manera, podemos ver que el <|>operador para Maybeactúa como concatenación para 'listas con como máximo un elemento'.

Warbo
fuente
1

En el espíritu de Conal, divídalo en operaciones más pequeñas y simples.

En este caso, asumdesde Data.Foldablehace la parte principal.

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

Alternativamente, a la respuesta de Jimmy Hoffa puede usar la Monoidinstancia, (->)pero luego necesita una Monoidinstancia Maybey la estándar no hace lo que desea. ¿Quieres Firstpartir Data.Monoid.

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

(O mconcatpara una versión anterior y más especializada de fold).

Derek Elkins dejó SE
fuente
Soluciones interesantes (+1). Encuentro el primero más intuitivo.
Giorgio