haskell - functions - ¿Cuál es la diferencia entre Pattern Matching y Guards?
functions haskell (4)
Para mí, parece que Pattern Matching and Guards son fundamentalmente los mismos. Ambos evalúan una condición, y si es verdadera ejecutará la expresión enganchada a ella. ¿Estoy en lo correcto en mi comprensión?
No exactamente. La primera coincidencia de patrón no puede evaluar las condiciones arbitrarias. Solo puede verificar si se creó un valor usando un constructor dado.
La segunda coincidencia de patrón puede vincular variables. Entonces, aunque el patrón []
podría ser equivalente al protector null lst
(sin usar la longitud porque eso no sería equivalente, más sobre eso más adelante), el patrón x:xs
ciertamente no es equivalente al protector not (null lst)
porque el patrón une las variables x
y xs
, que el guardia no.
Una nota sobre el uso de la length
: Usar la length
para verificar si una lista está vacía es una práctica muy mala porque, para calcular la longitud, debe pasar por toda la lista, lo que tomará O(n)
tiempo, mientras se comprueba si la lista está vacío toma O(1)
vez con null
o coincidencia de patrón. Además, usar `length'' simplemente no funciona en listas infinitas.
Soy muy nuevo en Haskell y en la programación funcional en general. Mi pregunta es bastante básica. ¿Cuál es la diferencia entre Pattern Matching y Guards?
Función usando coincidencia de patrón
check :: [a] -> String
check [] = "Empty"
check (x:xs) = "Contains Elements"
Función usando guardias
check_ :: [a] -> String
check_ lst
| length lst < 1 = "Empty"
| otherwise = "Contains elements"
Para mí, parece que Pattern Matching and Guards son fundamentalmente los mismos. Ambos evalúan una condición, y si es verdadera ejecutará la expresión enganchada a ella. ¿Estoy en lo correcto en mi comprensión?
En este ejemplo, puedo usar coincidencia de patrones o guardias para llegar al mismo resultado. Pero algo me dice que me estoy perdiendo algo importante aquí. ¿Podemos siempre reemplazar uno con el otro?
¿Podría alguien dar ejemplos en los que se prefiera la coincidencia de patrones con respecto a los protectores y viceversa?
¡En realidad, son fundamentalmente diferentes! Al menos en Haskell, en cualquier caso.
Los guardias son más simples y más flexibles: son esencialmente solo una sintaxis especial que se traduce en una serie de expresiones if / then. Puedes poner expresiones booleanas arbitrarias en las guardias, pero no hacen nada que no puedas hacer con un if
regular.
Las coincidencias de patrones hacen varias cosas adicionales: son la única forma de deconstruir datos , y vinculan identificadores dentro de su alcance . En el mismo sentido en que las protecciones son equivalentes a las expresiones if
, la coincidencia de patrones es equivalente a las expresiones de case
. Las declaraciones (ya sea en el nivel superior, o en algo así como una expresión let
) también son una forma de coincidencia de patrones, con las definiciones "normales" que coinciden con el patrón trivial, un único identificador.
Las coincidencias de patrones también tienden a ser la principal forma en que realmente sucede algo en Haskell: intentar deconstruir los datos en un patrón es una de las pocas cosas que fuerzan la evaluación.
Por cierto, puedes hacer una coincidencia de patrones en declaraciones de nivel superior:
square = (^2)
(one:four:nine:_) = map square [1..]
Esto ocasionalmente es útil para un grupo de definiciones relacionadas.
GHC también proporciona la extensión ViewPatterns que combina ambos; puede usar funciones arbitrarias en un contexto vinculante y luego coincide con el patrón en el resultado. Esto es solo azúcar sintáctico para las cosas habituales, por supuesto.
En cuanto a la cuestión del día a día para utilizar, aquí hay algunas guías básicas:
Definitivamente, utilice la coincidencia de patrones para cualquier cosa que se pueda combinar directamente con uno o dos constructores de profundidad, donde realmente no se preocupe por los datos compuestos como un todo, pero se preocupe por la mayor parte de la estructura. La sintaxis
@
permite vincular la estructura general a una variable a la vez que coincide con el patrón, pero hacer demasiado de eso en un patrón puede ponerse feo e ilegible rápidamente.Use protectores definitivamente cuando necesite hacer una elección basada en alguna propiedad que no se corresponda claramente con un patrón, por ejemplo, comparando dos valores
Int
para ver cuál es más grande.Si solo necesita un par de datos desde el interior de una estructura grande, particularmente si también necesita usar la estructura como un todo, las protecciones y las funciones de acceso son generalmente más legibles que algún patrón monstruoso lleno de
@
y_
.Si necesita hacer lo mismo para los valores representados por diferentes patrones, pero con un predicado conveniente para clasificarlos, usar un único patrón genérico con un guardia suele ser más legible. Tenga en cuenta que si un conjunto de protecciones no es exhaustivo, cualquier elemento que falle en todas las protecciones caerá al siguiente patrón (si lo hubiera). De modo que puede combinar un patrón general con un filtro para detectar casos excepcionales, luego haga una coincidencia de patrones en todo lo demás para obtener los detalles que le interesan.
Definitivamente no use protectores para cosas que podrían verificarse trivialmente con un patrón. La comprobación de listas vacías es el ejemplo clásico, use una coincidencia de patrón para eso.
En general, en caso de duda, simplemente mantente con la coincidencia de patrones por defecto, por lo general es más agradable. Si un patrón comienza a ponerse realmente feo o intrincado, entonces deténgase a considerar de qué otra manera podría escribirlo. Además de utilizar guardias, otras opciones incluyen extraer subexpresiones como funciones separadas o poner expresiones de
case
dentro del cuerpo de la función para poder aplicarles algo de la coincidencia de patrones y fuera de la definición principal.
Además de las otras buenas respuestas, trataré de ser específico sobre los guardias: los guardias son solo azúcar sintáctico. Si lo piensas, a menudo tendrás la siguiente estructura en tus programas:
f y = ...
f x =
if p(x) then A else B
Es decir, si un patrón coincide, es seguido inmediatamente por una discriminación if-then-else. Un guardia dobla esta discriminación directamente en el patrón:
f y = ...
f x | p(x) = A
| otherwise = B
(de lo otherwise
se define como True
en la biblioteca estándar). Es más conveniente que una cadena if-then-else y, a veces, también hace que el código sea mucho más simple en cuanto a la variante, por lo que es más fácil escribir que la construcción if-then-else.
En otras palabras, es azúcar en la parte superior de otra construcción de una manera que simplifica en gran medida su código en muchos casos. Descubrirá que elimina una gran cantidad de cadenas if-then-else y hace que su código sea más legible.
Por un lado, puede poner expresiones booleanas dentro de un guardia.
ActualizarAl igual que con las listas de comprensión, las expresiones booleanas se pueden mezclar libremente con las guardias de patrones. Por ejemplo:
f x | [y] <- x , y > 3 , Just z <- h y = ...
Hay una buena cita de Learn You a Haskell sobre la diferencia:
Mientras que los patrones son una forma de asegurarse de que un valor se ajuste a alguna forma y deconstruirlo, las guardias son una forma de comprobar si alguna propiedad de un valor (o varias de ellas) es verdadera o falsa. Eso se parece mucho a una declaración if y es muy similar. El caso es que los guardias son mucho más legibles cuando tienes varias condiciones y juegan muy bien con los patrones.