Haskell Language
Extensiones de lenguaje GHC comunes
Buscar..
Observaciones
Estas extensiones de idioma suelen estar disponibles cuando se utiliza el Compilador de Haskell de Glasgow (GHC), ya que no forman parte del Informe de idiomas de Haskell 2010 aprobado. Para usar estas extensiones, uno debe informar al compilador usando una bandera o colocar un programa de LANGUAGE
antes de la palabra clave del module
en un archivo. La documentación oficial se puede encontrar en la sección 7 de la guía del usuario de GCH.
El formato del programa LANGUAGE
es {-# LANGUAGE ExtensionOne, ExtensionTwo ... #-}
. Ese es el literal {-#
seguido de LANGUAGE
seguido por una lista de extensiones separadas por comas, y finalmente el #-}
cierre ». Se pueden colocar múltiples programas de LANGUAGE
en un archivo.
MultiParamTypeClasses
Es una extensión muy común que permite clases de tipo con múltiples parámetros de tipo. Puedes pensar en MPTC como una relación entre tipos.
{-# LANGUAGE MultiParamTypeClasses #-}
class Convertable a b where
convert :: a -> b
instance Convertable Int Float where
convert i = fromIntegral i
El orden de los parámetros importa.
Los MPTC a veces se pueden reemplazar con familias de tipos.
FlexibleInstances
Las instancias regulares requieren:
All instance types must be of the form (T a1 ... an)
where a1 ... an are *distinct type variables*,
and each type variable appears at most once in the instance head.
Eso significa que, por ejemplo, mientras puede crear una instancia para [a]
no puede crear una instancia específicamente para [Int]
. FlexibleInstances
relaja que:
class C a where
-- works out of the box
instance C [a] where
-- requires FlexibleInstances
instance C [Int] where
SobrecargadoStrings
Normalmente, los literales de cadena en Haskell tienen un tipo de String
(que es un alias de tipo para [Char]
). Si bien esto no es un problema para programas educativos más pequeños, las aplicaciones del mundo real a menudo requieren un almacenamiento más eficiente, como Text
o ByteString
.
OverloadedStrings
simplemente cambia el tipo de literales a
"test" :: Data.String.IsString a => a
Permitir que se pasen directamente a las funciones que esperan un tipo de este tipo. Muchas bibliotecas implementan esta interfaz para sus tipos de cadena, incluidos Data.Text y Data.ByteString, que proporcionan ciertas ventajas de tiempo y espacio sobre [Char]
.
También hay algunos usos únicos de OverloadedStrings
como los de la biblioteca Postgresql-simple que permite escribir consultas SQL entre comillas dobles como una cadena normal, pero brinda protección contra la concatenación incorrecta, una fuente notoria de ataques de inyección de SQL.
Para crear una instancia de la clase IsString
, debe fromString
función fromString
. Ejemplo † :
data Foo = A | B | Other String deriving Show
instance IsString Foo where
fromString "A" = A
fromString "B" = B
fromString xs = Other xs
tests :: [ Foo ]
tests = [ "A", "B", "Testing" ]
† Este ejemplo es cortesía de Lyndon Maydwell ( sordina
en GitHub) que se encuentra aquí .
TupleSections
Una extensión sintáctica que permite aplicar el constructor de tuplas (que es un operador) en una sección:
(a,b) == (,) a b
-- With TupleSections
(a,b) == (,) a b == (a,) b == (,b) a
N-tuplas
También funciona para tuplas con aridad mayor a dos.
(,2,) 1 3 == (1,2,3)
Cartografía
Esto puede ser útil en otros lugares donde se usan secciones:
map (,"tag") [1,2,3] == [(1,"tag"), (2, "tag"), (3, "tag")]
El ejemplo anterior sin esta extensión se vería así:
map (\a -> (a, "tag")) [1,2,3]
UnicodeSyntax
Una extensión que le permite usar caracteres Unicode en lugar de ciertos operadores y nombres integrados.
ASCII | Unicode | Usos) |
---|---|---|
:: | ∷ | tiene tipo |
-> | → | Tipos de funciones, lambdas, ramas de case , etc. |
=> | ⇒ | restricciones de clase |
forall | ∀ | polimorfismo explícito |
<- | ← | do notación |
* | ★ | el tipo (o tipo) de tipos (por ejemplo, Int :: ★ ) |
>- | ⤚ | notación proc para Arrows |
-< | ⤙ | notación proc para Arrows |
>>- | ⤜ | notación proc para Arrows |
-<< | ⤛ | notación proc para Arrows |
Por ejemplo:
runST :: (forall s. ST s a) -> a
se convertiría
runST ∷ (∀ s. ST s a) → a
Tenga en cuenta que el ejemplo *
vs. ★
es ligeramente diferente: ya que *
no está reservado, ★
también funciona de la misma manera que *
para la multiplicación, o cualquier otra función llamada (*)
, y viceversa. Por ejemplo:
ghci> 2 ★ 3
6
ghci> let (*) = (+) in 2 ★ 3
5
ghci> let (★) = (-) in 2 * 3
-1
Literales binarios
Haskell estándar le permite escribir literales enteros en decimal (sin ningún prefijo), hexadecimal (precedido por 0x
o 0X
) y octal (precedido por 0o
o 0O
). La extensión BinaryLiterals
agrega la opción de binario (precedida por 0b
o 0B
).
0b1111 == 15 -- evaluates to: True
Cuantificación existencial
Esta es una extensión de sistema de tipo que permite tipos que se cuantifican existencialmente, o, en otras palabras, tienen variables de tipo que solo se instancian en tiempo de ejecución † .
Un valor de tipo existencial es similar a una referencia de clase base-abstracta en idiomas OO: no sabe el tipo exacto que contiene, pero puede restringir la clase de tipos.
data S = forall a. Show a => S a
o equivalentemente, con sintaxis GADT:
{-# LANGUAGE GADTs #-}
data S where
S :: Show a => a -> S
Los tipos existenciales abren la puerta a cosas como contenedores casi heterogéneos: como se dijo anteriormente, en realidad puede haber varios tipos en un valor S
, pero todos pueden show
n, por lo tanto, también puede hacer
instance Show S where
show (S a) = show a -- we rely on (Show a) from the above
Ahora podemos crear una colección de tales objetos:
ss = [S 5, S "test", S 3.0]
Lo que también nos permite utilizar el comportamiento polimórfico:
mapM_ print ss
Los existenciales pueden ser muy poderosos, pero tenga en cuenta que en Haskell no son necesarios muy a menudo. En el ejemplo anterior, todo lo que puede hacer con la instancia de Show
es mostrar (¡duh!) Los valores, es decir, crear una representación de cadena. Por lo tanto, todo el tipo S
contiene exactamente tanta información como la cadena que se obtiene al mostrarla. Por lo tanto, generalmente es mejor simplemente almacenar esa cadena de inmediato, especialmente porque Haskell es perezoso y, por lo tanto, la cadena al principio solo será un thunk no evaluado de todos modos.
Por otro lado, los existenciales causan algunos problemas únicos. Por ejemplo, la forma en que la información de tipo está "oculta" en un existencial. Si coincide con un patrón en un valor de S
, tendrá el tipo contenido en el alcance (más precisamente, su instancia de Show
), pero esta información nunca puede escapar de su alcance, que por lo tanto se convierte en una "sociedad secreta": el compilador no permite que nada se escape del ámbito, excepto los valores cuyo tipo ya se conoce desde el exterior. Esto puede provocar errores extraños, como Couldn't match type 'a0' with '()' 'a0' is untouchable
.
† Contraste esto con el polimorfismo paramétrico ordinario, que generalmente se resuelve en el momento de la compilación (lo que permite el borrado de todo tipo).
Los tipos existenciales son diferentes de los tipos Rank-N: estas extensiones son, en términos generales, duales entre sí: para usar valores de un tipo existencial, necesita una función polimórfica (posiblemente restringida), como se show
en el ejemplo. Una función polimórfica se cuantifica universalmente , es decir, funciona para cualquier tipo en una clase dada, mientras que la cuantificación existencial significa que funciona para algún tipo particular que es a priori desconocido. Si tiene una función polimórfica, eso es suficiente, sin embargo, para pasar funciones polimórficas como argumentos, necesita {-# LANGUAGE Rank2Types #-}
:
genShowSs :: (∀ x . Show x => x -> String) -> [S] -> [String]
genShowSs f = map (\(S a) -> f a)
LambdaCase
Una extensión sintáctica que te permite escribir \case
en lugar de \arg -> case arg of
.
Considere la siguiente definición de función:
dayOfTheWeek :: Int -> String
dayOfTheWeek 0 = "Sunday"
dayOfTheWeek 1 = "Monday"
dayOfTheWeek 2 = "Tuesday"
dayOfTheWeek 3 = "Wednesday"
dayOfTheWeek 4 = "Thursday"
dayOfTheWeek 5 = "Friday"
dayOfTheWeek 6 = "Saturday"
Si desea evitar repetir el nombre de la función, puede escribir algo como:
dayOfTheWeek :: Int -> String
dayOfTheWeek i = case i of
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
Usando la extensión LambdaCase, puedes escribir eso como una expresión de función, sin tener que nombrar el argumento:
{-# LANGUAGE LambdaCase #-}
dayOfTheWeek :: Int -> String
dayOfTheWeek = \case
0 -> "Sunday"
1 -> "Monday"
2 -> "Tuesday"
3 -> "Wednesday"
4 -> "Thursday"
5 -> "Friday"
6 -> "Saturday"
RankNTypes
Imagina la siguiente situación:
foo :: Show a => (a -> String) -> String -> Int -> IO ()
foo show' string int = do
putStrLn (show' string)
putStrLn (show' int)
Aquí, queremos pasar una función que convierte un valor en una Cadena, aplicar esa función tanto a un parámetro de cadena como a un parámetro int e imprimirlos. En mi mente, no hay razón para que esto falle! Tenemos una función que funciona en ambos tipos de parámetros que estamos pasando.
Desafortunadamente, esto no va a escribir cheque! GHC infiere a
tipo basado en su primera aparición en el cuerpo de la función. Es decir, en cuanto nos topamos:
putStrLn (show' string)
GHC inferirá que show' :: String -> String
, ya que string
es una String
. Se procederá a estallar mientras se intenta show' int
.
RankNTypes
permite escribir la firma de tipo de la siguiente manera, cuantificando sobre todas las funciones que satisfacen el tipo show'
:
foo :: (forall a. Show a => (a -> String)) -> String -> Int -> IO ()
Este es el rango 2 polimorfismo: estamos afirmando que el show'
función debe funcionar para todos a
s dentro de nuestra función y la implementación anterior ahora trabaja.
La extensión RankNTypes
permite el anidamiento arbitrario de todos los bloques forall ...
en firmas de tipo. En otras palabras, permite el polimorfismo de rango N.
Listas sobrecargadas
añadido en GHC 7.8 .
OverloadedLists, similar a OverloadedStrings , permite que los literales de la lista se analicen de la siguiente manera:
[] -- fromListN 0 []
[x] -- fromListN 1 (x : [])
[x .. ] -- fromList (enumFrom x)
Esto es útil cuando se trata de tipos como Set
, Vector
y Map
s.
['0' .. '9'] :: Set Char
[1 .. 10] :: Vector Int
[("default",0), (k1,v1)] :: Map String Int
['a' .. 'z'] :: Text
IsList
clase IsList
en GHC.Exts
está diseñada para usarse con esta extensión.
IsList
está equipado con una función de tipo, Item
y tres funciones, fromList :: [Item l] -> l
, toList :: l -> [Item l]
y fromListN :: Int -> [Item l] -> l
donde fromListN
es opcional. Las implementaciones típicas son:
instance IsList [a] where
type Item [a] = a
fromList = id
toList = id
instance (Ord a) => IsList (Set a) where
type Item (Set a) = a
fromList = Set.fromList
toList = Set.toList
Ejemplos tomados de OverloadedLists - GHC .
Dependencias funcionales
Si tiene una clase de tipo de múltiples parámetros con los argumentos a, b, c y x, esta extensión le permite expresar que el tipo x se puede identificar de forma única a partir de a, b y c:
class SomeClass a b c x | a b c -> x where ...
Al declarar una instancia de dicha clase, se verificará contra todas las demás instancias para asegurarse de que se mantiene la dependencia funcional, es decir, no existe ninguna otra instancia con el mismo abc
pero diferente x
.
Puede especificar múltiples dependencias en una lista separada por comas:
class OtherClass a b c d | a b -> c d, a d -> b where ...
Por ejemplo en MTL podemos ver:
class MonadReader r m| m -> r where ...
instance MonadReader r ((->) r) where ...
Ahora, si tiene un valor de tipo MonadReader a ((->) Foo) => a
, el compilador puede inferir que a ~ Foo
, ya que el segundo argumento determina completamente el primero, y simplificará el tipo en consecuencia.
La clase SomeClass
se puede considerar como una función de los argumentos abc
que dan como resultado x
. Tales clases pueden usarse para hacer cálculos en el sistema de tipos.
GADTs
Los tipos de datos algebraicos convencionales son paramétricos en sus variables de tipo. Por ejemplo, si definimos un ADT como
data Expr a = IntLit Int
| BoolLit Bool
| If (Expr Bool) (Expr a) (Expr a)
con la esperanza de que esto descartará estáticamente condicionales no bien tipificados, esto no se comportará como se espera ya que el tipo de IntLit :: Int -> Expr a
se cuantifica IntLit :: Int -> Expr a
: para cualquier elección de a
, produce un valor de tipo Expr a
. En particular, para a ~ Bool
, tenemos IntLit :: Int -> Expr Bool
, que nos permite construir algo como If (IntLit 1) e1 e2
que es lo que el tipo del constructor If
intentaba descartar.
Los tipos de datos algebraicos generalizados nos permiten controlar el tipo resultante de un constructor de datos para que no sean meramente paramétricos. Podemos reescribir nuestro tipo Expr
como un GADT como este:
data Expr a where
IntLit :: Int -> Expr Int
BoolLit :: Bool -> Expr Bool
If :: Expr Bool -> Expr a -> Expr a -> Expr a
Aquí, el tipo del constructor IntLit
es Int -> Expr Int
, y así IntLit 1 :: Expr Bool
no verificará.
La coincidencia de patrones en un valor GADT provoca el refinamiento del tipo del término devuelto. Por ejemplo, es posible escribir un evaluador para Expr a
como este:
crazyEval :: Expr a -> a
crazyEval (IntLit x) =
-- Here we can use `(+)` because x :: Int
x + 1
crazyEval (BoolLit b) =
-- Here we can use `not` because b :: Bool
not b
crazyEval (If b thn els) =
-- Because b :: Expr Bool, we can use `crazyEval b :: Bool`.
-- Also, because thn :: Expr a and els :: Expr a, we can pass either to
-- the recursive call to `crazyEval` and get an a back
crazyEval $ if crazyEval b then thn else els
Tenga en cuenta que podemos usar (+)
en las definiciones anteriores porque cuando, por ejemplo, IntLit x
coincide con el patrón, también aprendemos que a ~ Int
(y lo mismo para not
y if_then_else_
cuando a ~ Bool
).
ScopedTypeVariables
ScopedTypeVariables
permite referirse a tipos cuantificados universalmente dentro de una declaración. Para ser más explícitos:
import Data.Monoid
foo :: forall a b c. (Monoid b, Monoid c) => (a, b, c) -> (b, c) -> (a, b, c)
foo (a, b, c) (b', c') = (a :: a, b'', c'')
where (b'', c'') = (b <> b', c <> c') :: (b, c)
Lo importante es que podemos utilizar a
, b
y c
para indicar al compilador en subexpresiones de la declaración (la tupla en el where
cláusula y el primero a
en el resultado final). En la práctica, ScopedTypeVariables
ayuda a escribir funciones complejas como una suma de partes, lo que permite al programador agregar firmas de tipo a valores intermedios que no tienen tipos concretos.
Sinónimo de patrones
Los sinónimos de patrón son abstracciones de patrones similares a cómo las funciones son abstracciones de expresiones.
Para este ejemplo, veamos la interfaz que expone Data.Sequence
y veamos cómo se puede mejorar con los sinónimos de patrón. El tipo Seq
es un tipo de datos que, internamente, utiliza una representación complicada para lograr una buena complejidad asintótica para varias operaciones, especialmente para O (1) (des) y un (des) snocing.
Pero esta representación es difícil de manejar y algunas de sus invariantes no pueden expresarse en el sistema de tipos de Haskell. Debido a esto, el tipo Seq
está expuesto a los usuarios como un tipo abstracto, junto con las funciones de constructor y de acceso que conservan invariantes, entre ellas:
empty :: Seq a
(<|) :: a -> Seq a -> Seq a
data ViewL a = EmptyL | a :< (Seq a)
viewl :: Seq a -> ViewL a
(|>) :: Seq a -> a -> Seq a
data ViewR a = EmptyR | (Seq a) :> a
viewr :: Seq a -> ViewR a
Pero usar esta interfaz puede ser un poco engorroso:
uncons :: Seq a -> Maybe (a, Seq a)
uncons xs = case viewl xs of
x :< xs' -> Just (x, xs')
EmptyL -> Nothing
Podemos usar patrones de vista para limpiarlo un poco:
{-# LANGUAGE ViewPatterns #-}
uncons :: Seq a -> Maybe (a, Seq a)
uncons (viewl -> x :< xs) = Just (x, xs)
uncons _ = Nothing
Usando la extensión de lenguaje PatternSynonyms
, podemos proporcionar una interfaz aún más agradable al permitir que la coincidencia de patrones pretenda que tenemos una lista de contras o de snoc:
{-# LANGUAGE PatternSynonyms #-}
import Data.Sequence (Seq)
import qualified Data.Sequence as Seq
pattern Empty :: Seq a
pattern Empty <- (Seq.viewl -> Seq.EmptyL)
pattern (:<) :: a -> Seq a -> Seq a
pattern x :< xs <- (Seq.viewl -> x Seq.:< xs)
pattern (:>) :: Seq a -> a -> Seq a
pattern xs :> x <- (Seq.viewr -> xs Seq.:> x)
Esto nos permite escribir uncons
en un estilo muy natural:
uncons :: Seq a -> Maybe (a, Seq a)
uncons (x :< xs) = Just (x, xs)
uncons _ = Nothing
RecordWildCards
Ver RecordWildCards