Guardias frente a si-entonces-si no frente a casos en Haskell

104

Tengo tres funciones que encuentran el enésimo elemento de una lista:

nthElement :: [a] -> Int -> Maybe a 
nthElement [] a = Nothing
nthElement (x:xs) a | a <= 0 = Nothing
                    | a == 1 = Just x
                    | a > 1 = nthElement xs (a-1)

nthElementIf :: [a] -> Int -> Maybe a
nthElementIf [] a = Nothing
nthElementIf (x:xs) a = if a <= 1
                        then if a <= 0 
                             then Nothing
                             else Just x -- a == 1
                        else nthElementIf xs (a-1)                           

nthElementCases :: [a] -> Int -> Maybe a
nthElementCases [] a = Nothing
nthElementCases (x:xs) a = case a <= 0 of
                             True -> Nothing
                             False -> case a == 1 of
                                        True -> Just x
                                        False -> nthElementCases xs (a-1)

En mi opinión, la primera función es la mejor implementación porque es la más concisa. Pero, ¿hay algo en las otras dos implementaciones que las haga preferibles? Y, por extensión, ¿cómo elegiría entre usar guardias, declaraciones if-then-else y casos?

nucleartida
fuente
5
puede colapsar sus casedeclaraciones anidadas si utilizócase compare a 0 of LT -> ... | EQ -> ... | GT -> ...
rampion
5
@rampion: quieres decircase compare a 1 of ...
newacct

Respuestas:

121

Desde un punto de vista técnico, las tres versiones son equivalentes.

Dicho esto, mi regla general para los estilos es que si puedes leerlo como si fuera en inglés (léelo |como "cuando", | otherwisecomo "de lo contrario" y =como "es" o "ser"), probablemente estés haciendo algo. Derecha.

if..then..elsees para cuando tiene una condición binaria o una sola decisión que debe tomar. Las if..then..elseexpresiones anidadas son muy poco comunes en Haskell, y casi siempre se deben usar guardias en su lugar.

let absOfN =
  if n < 0 -- Single binary expression
  then -n
  else  n

Cada if..then..elseexpresión puede ser reemplazada por una guardia si está en el nivel superior de una función, y esto generalmente debería ser preferido, ya que puede agregar más casos más fácilmente que:

abs n
  | n < 0     = -n
  | otherwise =  n

case..ofes para cuando tiene múltiples rutas de código y cada ruta de código está guiada por la estructura de un valor, es decir, a través de la coincidencia de patrones. Rara vez coincide con Truey False.

case mapping of
  Constant v -> const v
  Function f -> map f

Los guardias complementan las case..ofexpresiones, lo que significa que si necesita tomar decisiones complicadas en función de un valor, primero tome decisiones según la estructura de su entrada y luego tome decisiones sobre los valores en la estructura.

handle  ExitSuccess = return ()
handle (ExitFailure code)
  | code < 0  = putStrLn . ("internal error " ++) . show . abs $ code
  | otherwise = putStrLn . ("user error " ++)     . show       $ code

Por cierto. Como sugerencia de estilo, siempre haga una nueva línea después de a =o antes de a |si el contenido después de =/ |es demasiado largo para una línea, o usa más líneas por alguna otra razón:

-- NO!
nthElement (x:xs) a | a <= 0 = Nothing
                    | a == 1 = Just x
                    | a > 1 = nthElement xs (a-1)

-- Much more compact! Look at those spaces we didn't waste!
nthElement (x:xs) a
  | a <= 0    = Nothing
  | a == 1    = Just x
  | otherwise = nthElement xs (a-1)
dflemstr
fuente
1
"Rara vez encajas Truey False" ¿hay alguna ocasión en la que harías eso? Después de todo, este tipo de decisión siempre se puede tomar con un ify también con guardias.
izquierda rotonda alrededor del
2
Por ejemplocase (foo, bar, baz) of (True, False, False) -> ...
dflemstr
@dflemstr ¿No hay diferencias más sutiles, por ejemplo, los guardias que requieren MonadPlus y y devuelven una instancia de mónada mientras que if-then-else no? Pero no estoy seguro.
J Fritsch
2
@JFritsch: la guardfunción requiere MonadPlus, pero de lo que estamos hablando aquí es de guardias como en | test =cláusulas, que no están relacionadas.
Ben Millwood
Gracias por el consejo de estilo, ahora lo confirma la duda.
truthadjustr
22

Sé que esta es una pregunta sobre el estilo para funciones explícitamente recursivas, pero sugeriría que el mejor estilo es encontrar una manera de reutilizar las funciones recursivas existentes.

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)
Daniel Wagner
fuente
2

Esto es solo una cuestión de pedido, pero creo que es muy legible y tiene la misma estructura que los guardias.

nthElement :: [a] -> Int -> Maybe a 
nthElement [] a = Nothing
nthElement (x:xs) a = if a  < 1 then Nothing else
                      if a == 1 then Just x
                      else nthElement xs (a-1)

El último más no es necesario y si no hay otras posibilidades, también las funciones deben tener "caso de último recurso" en caso de que se haya perdido algo.

Cristian garcia
fuente
4
Las sentencias if anidadas son un anti-patrón cuando puede usar protectores de casos.
user76284