programar - palabras reservadas haskell
Funciones ocultas de Haskell (25)
¿Cuáles son las características menos conocidas pero útiles del lenguaje de programación Haskell? (Entiendo que el lenguaje en sí mismo es menos conocido, pero trabaje conmigo. Incluso las explicaciones de las cosas simples en Haskell, como definir la secuencia de Fibonacci con una línea de código, recibirán un voto favorable para mí).
- Intenta limitar las respuestas al núcleo de Haskell
- Una característica por respuesta
- Dé un ejemplo y una breve descripción de la característica, no solo un enlace a la documentación
- Etiquete la función usando el título en negrita como primera línea
Tipos de datos algebraicos generalizados. Aquí hay un intérprete de ejemplo donde el sistema de tipos le permite cubrir todos los casos:
{-# LANGUAGE GADTs #-}
module Exp
where
data Exp a where
Num :: (Num a) => a -> Exp a
Bool :: Bool -> Exp Bool
Plus :: (Num a) => Exp a -> Exp a -> Exp a
If :: Exp Bool -> Exp a -> Exp a -> Exp a
Lt :: (Num a, Ord a) => Exp a -> Exp a -> Exp Bool
Lam :: (a -> Exp b) -> Exp (a -> b) -- higher order abstract syntax
App :: Exp (a -> b) -> Exp a -> Exp b
-- deriving (Show) -- failse
eval :: Exp a -> a
eval (Num n) = n
eval (Bool b) = b
eval (Plus e1 e2) = eval e1 + eval e2
eval (If p t f) = eval $ if eval p then t else f
eval (Lt e1 e2) = eval e1 < eval e2
eval (Lam body) = /x -> eval $ body x
eval (App f a) = eval f $ eval a
instance Eq a => Eq (Exp a) where
e1 == e2 = eval e1 == eval e2
instance Show (Exp a) where
show e = "<exp>" -- very weak show instance
instance (Num a) => Num (Exp a) where
fromInteger = Num
(+) = Plus
let 5 = 6 in ...
es Haskell válido.
seq
y ($!)
solo evalúan lo suficiente como para comprobar que algo no está en el fondo.
El siguiente programa solo imprimirá "allí".
main = print "hi " `seq` print "there"
Para aquellos que no están familiarizados con Haskell, Haskell no es estricto en general, lo que significa que un argumento para una función solo se evalúa si es necesario.
Por ejemplo, lo siguiente imprime "ignorado" y termina con éxito.
main = foo (error "explode!")
where foo _ = print "ignored"
Se sabe que seq
cambia ese comportamiento evaluando hacia abajo si su primer argumento es inferior.
Por ejemplo:
main = error "first" `seq` print "impossible to print"
... o lo que es lo mismo, sin infijo ...
main = seq (error "first") (print "impossible to print")
... explotará con un error en "primero". Nunca se imprimirá "imposible de imprimir".
Por lo tanto, puede ser un poco sorprendente que aunque seq
sea estricto, no evalúe algo de la forma en que evalúan los lenguajes ansiosos. En particular, no intentará forzar todos los enteros positivos en el siguiente programa. En cambio, comprobará que [1..]
no esté en el fondo (que se puede encontrar inmediatamente), imprima "listo" y salga.
main = [1..] `seq` print "done"
Bonitos guardias
Prelude
define lo otherwise = True
, haciendo que las condiciones de guardia completas se lean de manera muy natural.
fac n
| n < 1 = 1
| otherwise = n * fac (n-1)
Coincidencia de patrones mejorada
- Patrones perezosos
Patrones irrefutables
let ~(Just x) = someExpression
Comentarios multiline encajables .
{- inside a comment,
{- inside another comment, -}
still commented! -}
Composición de la función legible
Prelude
define (.)
Como composición de función matemática; es decir, g . f
Primero aplica f
, luego aplica g
al resultado.
Si import Control.Arrow
, los siguientes son equivalentes:
g . f
f >>> g
Control.Arrow
proporciona una instance Arrow (->)
, y esto es bueno para las personas que no les gusta leer la aplicación de funciones al revés.
Comprensión de la lista paralela
(Característica GHC especial)
fibs = 0 : 1 : [ a + b | a <- fibs | b <- tail fibs ]
Diseño opcional
Puede usar llaves explícitas y puntos y comas en lugar de espacios en blanco (también conocido como diseño) para delimitar bloques.
let {
x = 40;
y = 2
} in
x + y
... o equivalente...
let { x = 40; y = 2 } in x + y
... en lugar de ...
let x = 40
y = 2
in x + y
Debido a que no se requiere diseño, los programas de Haskell pueden ser producidos directamente por otros programas.
Enumeraciones de estilo C
La combinación de coincidencia de patrones de nivel superior y secuencias aritméticas nos da una forma práctica de definir valores consecutivos:
foo : bar : baz : _ = [100 ..] -- foo = 100, bar = 101, baz = 102
Enumeraciones
Cualquier tipo que sea una instancia de Enum puede usarse en una secuencia aritmética, no solo en números:
alphabet :: String
alphabet = [''A'' .. ''Z'']
Incluyendo sus propios tipos de datos, solo se derivan de Enum para obtener una implementación predeterminada:
data MyEnum = A | B | C deriving(Eq, Show, Enum)
main = do
print $ [A ..] -- prints "[A,B,C]"
print $ map fromEnum [A ..] -- prints "[0,1,2]"
Especificación flexible de las importaciones y exportaciones de módulos
Importar y exportar es bueno.
module Foo (module Bar, blah) -- this is module Foo, export everything that Bar expored, plus blah
import qualified Some.Long.Name as Short
import Some.Long.Name (name) -- can import multiple times, with different options
import Baz hiding (blah) -- import everything from Baz, except something named ''blah''
Estructuras de control definidas por el usuario
Haskell no tiene un operador ternario taquigráfico. El if
- then
- else
incorporado es siempre ternario, y es una expresión (los lenguajes imperativos tienden a tener ?:
= Expresión, if
= enunciado). Si quieres, sin embargo,
True ? x = const x
False ? _ = id
definirá (?)
para ser el operador ternario:
(a ? b $ c) == (if a then b else c)
Tendría que recurrir a macros en la mayoría de los demás lenguajes para definir sus propios operadores lógicos en cortocircuito, pero Haskell es un lenguaje totalmente vago, por lo que simplemente funciona.
-- prints "I''m alive! :)"
main = True ? putStrLn "I''m alive! :)" $ error "I''m dead :("
Evitando paréntesis
Las funciones (.)
Y ($)
en Prelude
tienen una solución muy conveniente, lo que le permite evitar paréntesis en muchos lugares. Los siguientes son equivalentes:
f (g (h x))
f $ g $ h x
f . g $ h x
f . g . h $ x
flip
también ayuda, los siguientes son equivalentes:
map (/a -> {- some long expression -}) list
flip map list $ /a ->
{- some long expression -}
Fixity del operador
Puede usar las palabras clave infix, infixl o infixr para definir la asociatividad y la precedencia de los operadores. Ejemplo tomado de la reference :
main = print (1 +++ 2 *** 3)
infixr 6 +++
infixr 7 ***,///
(+++) :: Int -> Int -> Int
a +++ b = a + 2*b
(***) :: Int -> Int -> Int
a *** b = a - 4*b
(///) :: Int -> Int -> Int
a /// b = 2*a - 3*b
Output: -19
El número (0 a 9) después del infijo le permite definir la precedencia del operador, siendo 9 el más fuerte. Infix no significa asociatividad, mientras que infixl asocia a la izquierda e infixr asocia a la derecha.
Esto le permite definir operadores complejos para realizar operaciones de alto nivel escritas como expresiones simples.
Tenga en cuenta que también puede usar funciones binarias como operadores si las coloca entre palos de retroceso:
main = print (a `foo` b)
foo :: Int -> Int -> Int
foo a b = a + b
Y como tal, también puede definir la precedencia para ellos:
infixr 4 `foo`
Hoogle
Hoogle es tu amigo. Lo admito, no es parte del "núcleo", así que cabal install hoogle
Ahora ya sabes cómo "si estás buscando una función de orden superior, ya está allí" ( comentario de ephemient ). ¿Pero cómo encuentras esa función? ¡Con hoogle!
$ hoogle "Num a => [a] -> a"
Prelude product :: Num a => [a] -> a
Prelude sum :: Num a => [a] -> a
$ hoogle "[Maybe a] -> [a]"
Data.Maybe catMaybes :: [Maybe a] -> [a]
$ hoogle "Monad m => [m a] -> m [a]"
Prelude sequence :: Monad m => [m a] -> m [a]
$ hoogle "[a] -> [b] -> (a -> b -> c) -> [c]"
Prelude zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
El programador de hoogle-google no puede escribir sus programas en papel por sí mismo de la misma manera que lo hace con la ayuda de la computadora. Pero él y la máquina juntos son un forzado no * a tener en cuenta.
Por cierto, si te gustaba hoogle, asegúrate de echar un vistazo a Hlint!
Listas infinitas
Como mencionó fibonacci, hay una manera muy elegante de generar números de fibonacci de una lista infinita como esta:
fib@(1:tfib) = 1 : 1 : [ a+b | (a,b) <- zip fib tfib ]
El operador @ le permite usar la coincidencia de patrones en la estructura 1: tfib mientras sigue refiriéndose al patrón completo como fib.
Tenga en cuenta que la lista de comprensión entra en una recursión infinita, generando una lista infinita. Sin embargo, puede solicitar elementos o utilizarlos, siempre que solicite una cantidad limitada:
take 10 fib
También puede aplicar una operación a todos los elementos antes de solicitarlos:
take 10 (map (/x -> x+1) fib)
Esto es gracias a la evaluación perezosa de parámetros y listas de Haskell.
Mónadas
No están tan escondidos, pero están simplemente en todas partes, incluso donde no pienses en ellos (Lists, Maybe-Types) ...
Mi cerebro acaba de explotar
Si intenta compilar este código:
{-# LANGUAGE ExistentialQuantification #-}
data Foo = forall a. Foo a
ignorefoo f = 1 where Foo a = f
Obtendrá este mensaje de error:
$ ghc Foo.hs Foo.hs:3:22: My brain just exploded. I can''t handle pattern bindings for existentially-quantified constructors. Instead, use a case-expression, or do-notation, to unpack the constructor. In the binding group for Foo a In a pattern binding: Foo a = f In the definition of `ignorefoo'': ignorefoo f = 1 where Foo a = f
Patrones en enlaces de nivel superior
five :: Int
Just five = Just 5
a, b, c :: Char
[a,b,c] = "abc"
¡Cuan genial es eso! Le ahorra esa llamada de fromJust
y head
vez en cuando.
Razonamiento ecuacional
Haskell, al ser puramente funcional, le permite leer un signo igual como un signo real igual (en ausencia de patrones no superpuestos).
Esto le permite sustituir definiciones directamente en el código, y en términos de optimización le da mucho margen de maniobra al compilador sobre cuándo sucede algo.
Un buen ejemplo de esta forma de razonamiento se puede encontrar aquí:
http://www.haskell.org/pipermail/haskell-cafe/2009-March/058603.html
Esto también se manifiesta muy bien en forma de leyes o REGLAS pragmas esperados para los miembros válidos de una instancia, por ejemplo, las leyes de Mónada:
- retornar a >> = f == fa
- m >> = return == m
- (m >> = f) >> = g == m >> = (/ x -> fx >> = g)
a menudo se puede utilizar para simplificar el código monádico.
Si está buscando una lista o una función de orden superior, ya está allí
Hay muchas funciones de conveniencia y de orden superior en la biblioteca estándar.
-- factorial can be written, using the strict HOF foldl'':
fac n = Data.List.foldl'' (*) 1 [1..n]
-- there''s a shortcut for that:
fac n = product [1..n]
-- and it can even be written pointfree:
fac = product . enumFromTo 1
Taquigrafía para una operación de lista común
Los siguientes son equivalentes:
concat $ map f list
concatMap f list
list >>= f
Editar
Dado que se solicitaron más detalles ...
concat :: [[a]] -> [a]
concat
toma una lista de listas y las concatena en una sola lista.
map :: (a -> b) -> [a] -> [b]
map
mapea una función sobre una lista.
concatMap :: (a -> [b]) -> [a] -> [b]
concatMap
es equivalente a (.) concat . map
(.) concat . map
: asigna una función sobre una lista y concatenan los resultados.
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
return :: a -> m a
Una Monad
tiene una operación de enlace , que se llama >>=
en Haskell (o su equivalente en azucar). List, aka []
, es una Monad
. Si sustituimos []
por m
en lo anterior:
instance Monad [] where
(>>=) :: [a] -> (a -> [b]) -> [b]
return :: a -> [a]
¿Qué es lo natural para las operaciones de Monad
en una lista? Tenemos que satisfacer las leyes de la mónada,
return a >>= f == f a
ma >>= (/a -> return a) == ma
(ma >>= f) >>= g == ma >>= (/a -> f a >>= g)
Puede verificar si estas leyes son válidas si usamos la implementación
instance Monad [] where
(>>=) = concatMap
return = (:[])
return a >>= f == [a] >>= f == concatMap f [a] == f a
ma >>= (/a -> return a) == concatMap (/a -> [a]) ma == ma
(ma >>= f) >>= g == concatMap g (concatMap f ma) == concatMap (concatMap g . f) ma == ma >>= (/a -> f a >>= g)
Este es, de hecho, el comportamiento de Monad []
. Como una demostración,
double x = [x,x]
main = do
print $ map double [1,2,3]
-- [[1,1],[2,2],[3,3]]
print . concat $ map double [1,2,3]
-- [1,1,2,2,3,3]
print $ concatMap double [1,2,3]
-- [1,1,2,2,3,3]
print $ [1,2,3] >>= double
-- [1,1,2,2,3,3]
Teoremas libres
Phil Wadler nos presentó la noción de un teorema libre y desde entonces hemos estado abusando de ellos en Haskell.
Estos maravillosos artefactos de los sistemas tipo Hindley-Milner ayudan con el razonamiento ecuacional al usar parametricidad para informarle sobre lo que una función no hará.
Por ejemplo, hay dos leyes que cada instancia de Functor debería cumplir:
- forall f g. fmap f. fmap g = fmap (f. g)
- fmap id = id
Pero, el teorema libre nos dice que no tenemos que molestarnos en probar el primero, ¡pero dado el segundo, es ''gratis'' solo por la firma del tipo!
fmap :: Functor f => (a -> b) -> f a -> f b
Debe ser un poco cuidadoso con la pereza, pero esto está parcialmente cubierto en el documento original, y en el documento más reciente de Janis Voigtlaender sobre teoremas libres en presencia de las seq
.
pereza
La holgazanería omnipresente significa que puedes hacer cosas como definir
fibs = 1 : 1 : zipWith (+) fibs (tail fibs)
Pero también nos proporciona muchos más beneficios sutiles en términos de sintaxis y razonamiento.
Por ejemplo, debido a la rigurosidad, ML tiene que lidiar con la restricción de valores , y tiene mucho cuidado de rastrear los enlaces circulares, pero en Haskell, podemos permitir que cada let sea recursivo y no tengamos necesidad de distinguir entre val
y fun
. Esto elimina una gran verruga sintáctica del lenguaje.
Esto da lugar indirectamente a nuestra encantadora cláusula where
, porque podemos mover con seguridad los cálculos que pueden o no ser utilizados fuera del flujo de control principal y permitir que la pereza se ocupe de compartir los resultados.
Podemos reemplazar (casi) todas las funciones de estilo ML que necesitan tomar () y devolver un valor, con solo un cálculo lento del valor. Hay razones para evitar hacerlo de vez en cuando para evitar pérdidas de espacio con los CAFs , pero estos casos son raros.
Finalmente, permite una reducción de eta ilimitada ( /x -> fx
puede reemplazarse por f). Esto hace que la programación orientada a combinador para cosas como los combinadores de analizador sea mucho más agradable que trabajar con construcciones similares en un lenguaje estricto.
Esto lo ayuda a razonar acerca de los programas en un estilo sin puntos, o al reescribirlos en un estilo sin puntos y reduce el ruido de los argumentos.