Pregunta

¿Cuáles son las características menos conocidas pero útiles del lenguaje de programación Haskell? (Entiendo que el lenguaje en sí 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 mi voto).

  • Intente limitar las respuestas al núcleo de Haskell
  • Una característica por respuesta
  • Dé un ejemplo y una breve descripción de la función, no solo un enlace a la documentación
  • Etiquete la función con el título en negrita como la primera línea
¿Fue útil?

Solución

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

Recibirá 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

Otros consejos

Estructuras de control definidas por el usuario

Haskell no tiene un operador ternario de taquigrafía. El if incorporado entonces - else siempre es ternario y es una expresión (los lenguajes imperativos tienden a tener ?: = expresión, if = instrucción). Sin embargo, si quieres,

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 otros idiomas para definir sus propios operadores lógicos de cortocircuito, pero Haskell es un lenguaje totalmente perezoso, así que simplemente funciona.

-- prints "I'm alive! :)"
main = True ? putStrLn "I'm alive! :)" $ error "I'm dead :("

Hoogle

Hoogle es tu amigo. Admito que no forma parte del " Core " ;, por lo que cabal install hoogle

Ahora sabe cómo " si está 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 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 obligados a no ser considerados.

Por cierto, si te gustó hoogle, ¡asegúrate de consultar hlint!

Teoremas gratuitos

Phil Wadler nos presentó la noción de un teorema gratuito y hemos estado abusando de ellos en Haskell desde entonces.

Estos maravillosos artefactos de los sistemas de tipo estilo Hindley-Milner ayudan con el razonamiento equitativo mediante el uso de la parametricidad para informarle sobre lo que una función no hará.

Por ejemplo, hay dos leyes que cada instancia de Functor debe cumplir:

  1. para todos f g. fmap f. fmap g = fmap (f. g)
  2. fmap id = id

Pero, el teorema gratuito nos dice que no debemos molestarnos en probar el primero, sino que, dado el segundo, ¡es gratis 'solo por la firma de tipo!

fmap :: Functor f => (a -> b) -> f a -> f b

Debe tener un poco de cuidado con la pereza, pero esto está parcialmente cubierto en el documento original, y en documento más reciente sobre teoremas libres en presencia de seq .

Abreviatura 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 asigna una función sobre una lista.

concatMap :: (a -> [b]) -> [a] -> [b]

concatMap es equivalente a (.) concat. map : mapea una función sobre una lista y concatena los resultados.

class Monad m where
    (>>=) :: m a -> (a -> m b) -> m b
    return :: a -> m a

Una Monad tiene una operación bind , que se llama > > = en Haskell (o su azucarado do -equivalente). La lista, también conocida como [] , es una Monad . Si sustituimos [] por m en lo anterior:

instance Monad [] where
    (>>=) :: [a] -> (a -> [b]) -> [b]
    return :: a -> [a]

¿Qué es lo natural que deben hacer las operaciones de Monad en una lista? Tenemos que cumplir con 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 que estas leyes se cumplen 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 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]

Comentarios multilínea anidables .

{- inside a comment,
     {- inside another comment, -}
still commented! -}

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

Patrones en enlaces de nivel superior

five :: Int
Just five = Just 5

a, b, c :: Char
[a,b,c] = "abc"

¡Qué bueno es eso! Le ahorra esa llamada a fromJust y head de vez en cuando.

Diseño opcional

Puede usar llaves y puntos y coma explícitos 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 el diseño no es necesario, los programas de Haskell pueden ser producidos directamente por otros programas.

seq y ($!) solo evalúa lo suficiente como para comprobar que algo no está en el fondo.

El siguiente programa solo se 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 a una función solo se evalúa si es necesario.

Por ejemplo, las siguientes impresiones " ignoradas " y termina con éxito.

main = foo (error "explode!")
  where foo _ = print "ignored"
Se sabe que

seq cambia ese comportamiento evaluando a fondo si su primer argumento es bottom.

Por ejemplo:

main = error "first" `seq` print "impossible to print"

... o equivalentemente, 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, a pesar de que seq es estricto, no evaluará algo de la misma manera que evalúan los lenguajes ansiosos. En particular, no intentará forzar todos los enteros positivos en el siguiente programa. En su lugar, verificará que [1 ..] no esté en la parte inferior (que se puede encontrar de inmediato), imprima " listo " y salga.

main = [1..] `seq` print "done"

Solidez del operador

Puede usar las palabras clave infix, infixl o infixr para definir operadores asociatividad y precedencia. Ejemplo tomado de la referencia :

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 significa que no hay asociatividad, mientras que infixl se asocia a la izquierda y a infixr se 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 los coloca entre comillas invertidas:

main = print (a `foo` b)

foo :: Int -> Int -> Int
foo a b = a + b

Y como tal, también puede definir la prioridad para ellos:

infixr 4 `foo`

Evitar paréntesis

Las funciones (.) y ($) en Prelude tienen fijaciones muy convenientes, 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 -}

Guardias bonitos

Prelude define de lo contrario = True , lo que hace que las condiciones completas de la guardia se lean de forma muy natural.

fac n
  | n < 1     = 1
  | otherwise = n * fac (n-1)

Enumeración de estilo C

La combinación de secuencias de aritmética y coincidencia de patrones de nivel superior nos brinda una forma práctica de definir valores consecutivos:

foo : bar : baz : _ = [100 ..]    -- foo = 100, bar = 101, baz = 102

Composición de funciones legibles

Prelude define (.) como composición de funciones matemáticas; es decir, g. f primero aplica f , luego aplica g al resultado.

Si importa Control.Arrow , los siguientes son equivalentes:

g . f
f >>> g

Control.Arrow proporciona una flecha de instancia (- >) , y esto es bueno para las personas a las que no les gusta leer la aplicación de funciones al revés.

deja que 5 = 6 en ... es válido Haskell.

Listas infinitas

Como mencionó fibonacci, hay una manera muy elegante de generando 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 sin dejar de referirse a todo el patrón 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 de ellos u operarlos, siempre que solicite una cantidad finita:

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 vaga evaluación de parámetros y listas de Haskell.

Especificación flexible de importación y exportación 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'

Si está buscando una lista o una función de orden superior, ya está allí

En la biblioteca estándar hay muchas otras funciones de conveniencia y orden superior.

-- 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

Razonamiento equacional

Haskell, siendo puramente funcional te permite leer un signo igual como un signo igual real (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 al compilador sobre cuándo suceden las cosas.

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 pragmas de REGLAS esperados para miembros válidos de una instancia, por ejemplo, las leyes de la mónada:

  1. regresó a > > = f == f a
  2. m > > = return == m
  3. (m > > = f) > > = g == m > > = (\ x - > f x > > = g)

a menudo se puede utilizar para simplificar el código monádico.

Laziness

La pereza ubicua 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 rigidez, ML tiene que tratar con el restricción de valor , y es muy cuidadoso al rastrear los enlaces circulares, pero en Haskell, podemos dejar que cada uno sea recursivo y no tenemos que distinguir entre val y fun . Esto elimina una verruga sintáctica importante del lenguaje.

Esto da lugar indirectamente a nuestra encantadora cláusula where , porque podemos mover de manera segura los cálculos que pueden o no ser utilizados fuera del flujo de control principal y dejar que la pereza se ocupe de compartir los resultados.

Podemos reemplazar (casi) todas esas funciones de estilo ML que necesitan tomar () y devolver un valor, con solo un cálculo perezoso del valor. Hay razones para evitar hacerlo de vez en cuando para evitar fugas de espacio con CAFs , pero tales los casos son raros.

Finalmente, permite una reducción eta sin restricciones ( \ x - > f x se puede reemplazar con f). Esto hace que la programación orientada al combinador para cosas como los combinadores analizadores sea mucho más agradable que trabajar con construcciones similares en un lenguaje estricto.

Esto le ayuda a razonar sobre programas en un estilo sin puntos, o sobre reescribirlos en un estilo sin puntos y reduce el ruido de los argumentos.

Comprensión de la lista paralela

(Característica especial de GHC)

  fibs = 0 : 1 : [ a + b | a <- fibs | b <- tail fibs ]

Coincidencia de patrones mejorada

  • Patrones perezosos
  • Patrones irrefutables

    let ~(Just x) = someExpression
    

Consulte coincidencia de patrones

Enumeraciones

Cualquier tipo que sea una instancia de Enum puede usarse en una secuencia aritmética, no solo números:

alphabet :: String
alphabet = ['A' .. 'Z']

Incluyendo sus propios tipos de datos, simplemente derive 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]"

Monads

No están tan ocultos, pero están simplemente en todas partes, incluso donde no piensas en ellos (Listas, Quizás-Tipos) ...

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top