Los guardias vs if-then-else vs los casos en Haskell


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ías entre usar guardias, declaraciones if-then-else y casos?

Author: Gilles, 2012-02-19

3 answers

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 inglés (lee | como "when", | otherwise como "otherwise" y = como "is" o "be"), probablemente estés haciendo algo bien.

if..then..else es para cuando tienes una condición binaria, o una sola decisión que necesitas tomar. Anidadas if..then..else - las expresiones son muy poco comunes en Haskell, y las guardas deberían ser usadas casi siempre en su lugar.

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

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

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

case..of es para cuando tiene múltiples rutas de código, y cada ruta de código es guiada por el estructura de un valor, es decir, mediante coincidencia de patrones. Rara vez coincides con True y False.

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

Guardias complementan case..of expresiones, significado que si necesita tomar decisiones complicadas dependiendo de un valor, primero tome decisiones dependiendo de 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

BTW. Como un consejo de estilo, siempre hacer una nueva línea después de un = o antes de un | si el =/| 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)
 104
Author: dflemstr,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-19 01:15:02

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

nthElement xs n = guard (n > 0) >> listToMaybe (drop (n-1) xs)
 21
Author: Daniel Wagner,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2012-02-19 02:36:07

Esto es solo una cuestión de orden, 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 necesita y si ya no hay otras posibilidades, también las funciones deben tener "caso de último recurso" en caso de que se haya perdido algo.

 1
Author: Cristian Garcia,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/ajaxhispano.com/template/agent.layouts/content.php on line 61
2014-05-22 21:19:14