Esse @n
é um recurso avançado do Haskell moderno, que geralmente não é coberto por tutoriais como o LYAH, nem pode ser encontrado no Relatório.
É chamado de aplicativo de tipo e é uma extensão de idioma do GHC. Para entendê-lo, considere esta função polimórfica simples
dup :: forall a . a -> (a, a)
dup x = (x, x)
A chamada intuitiva dup
funciona da seguinte maneira:
- o chamador escolhe um tipo
a
- o chamador escolhe um valor
x
do tipo escolhido anteriormentea
dup
então responde com um valor do tipo (a,a)
Em certo sentido, são dup
necessários dois argumentos: o tipo a
e o valor x :: a
. No entanto, o GHC geralmente é capaz de inferir o tipo a
(por exemplo x
, de ou do contexto em que estamos usando dup
), portanto, geralmente passamos apenas um argumento para dup
, a saber x
. Por exemplo, temos
dup True :: (Bool, Bool)
dup "hello" :: (String, String)
...
Agora, e se queremos passar a
explicitamente? Bem, nesse caso, podemos ativar a TypeApplications
extensão e escrever
dup @Bool True :: (Bool, Bool)
dup @String "hello" :: (String, String)
...
Observe os @...
argumentos que carregam tipos (não valores). Isso é algo que existe apenas no tempo de compilação - no tempo de execução, o argumento não existe.
Por que queremos isso? Bem, às vezes não há por x
perto, e queremos estimular o compilador a escolher o certo a
. Por exemplo
dup @Bool :: Bool -> (Bool, Bool)
dup @String :: String -> (String, String)
...
Os aplicativos de tipo geralmente são úteis em combinação com outras extensões que inviabilizam a inferência de tipo para o GHC, como tipos ambíguos ou famílias de tipos. Não vou discutir isso, mas você pode simplesmente entender que, às vezes, você realmente precisa ajudar o compilador, especialmente ao usar recursos avançados de tipo.
Agora, sobre o seu caso específico. Não tenho todos os detalhes, não conheço a biblioteca, mas é muito provável que você n
represente um tipo de valor de número natural no nível de tipo . Aqui estamos mergulhando em extensões bastante avançadas, como as mencionadas acima DataKinds
, talvez GADTs
, e algumas máquinas de escrever. Embora eu não possa explicar tudo, espero que eu possa fornecer algumas informações básicas. Intuitivamente,
foo :: forall n . some type using n
toma como argumento @n
, um tipo de tempo de compilação natural, que não é passado no tempo de execução. Em vez de,
foo :: forall n . C n => some type using n
takes @n
(tempo de compilação), juntamente com uma prova que n
satisfaça a restrição C n
. O último é um argumento de tempo de execução, que pode expor o valor real de n
. De fato, no seu caso, acho que você tem algo que se parece vagamente
value :: forall n . Reflects n Int => Int
que essencialmente permite que o código traga o nível de tipo natural para o nível de termo, acessando essencialmente o "tipo" como um "valor". (O tipo acima é considerado um "ambíguo", a propósito - você realmente precisa @n
desambiguar.)
Finalmente: por que alguém deveria querer passar n
no nível de tipo se depois convertemos isso para o nível de termo? Não seria mais fácil simplesmente escrever funções como
foo :: Int -> ...
foo n ... = ... use n
em vez do mais pesado
foo :: forall n . Reflects n Int => ...
foo ... = ... use (value @n)
A resposta honesta é: sim, seria mais fácil. No entanto, ter n
no nível de tipo permite ao compilador executar mais verificações estáticas. Por exemplo, você pode querer um tipo para representar "módulo inteiro n
" e permitir a adição deles. Tendo
data Mod = Mod Int -- Int modulo some n
foo :: Int -> Mod -> Mod -> Mod
foo n (Mod x) (Mod y) = Mod ((x+y) `mod` n)
funciona, mas não há verificação disso x
e y
são do mesmo módulo. Podemos adicionar maçãs e laranjas, se não tomarmos cuidado. Poderíamos escrever
data Mod n = Mod Int -- Int modulo n
foo :: Int -> Mod n -> Mod n -> Mod n
foo n (Mod x) (Mod y) = Mod ((x+y) `mod` n)
o que é melhor, mas ainda permite ligar foo 5 x y
mesmo quando n
não é 5
. Não é bom. Em vez de,
data Mod n = Mod Int -- Int modulo n
-- a lot of type machinery omitted here
foo :: forall n . SomeConstraint n => Mod n -> Mod n -> Mod n
foo (Mod x) (Mod y) = Mod ((x+y) `mod` (value @n))
impede que as coisas dêem errado. O compilador verifica estaticamente tudo. O código é mais difícil de usar, sim, mas de certa forma dificultar o uso é o ponto principal: queremos tornar impossível para o usuário tentar adicionar algo do módulo errado.
Concluindo: estas são extensões muito avançadas. Se você é iniciante, precisará progredir lentamente em direção a essas técnicas. Não desanime se você não conseguir compreendê-los após apenas um breve estudo, isso leva algum tempo. Faça um pequeno passo de cada vez, resolva alguns exercícios para cada recurso para entender o objetivo. E você sempre terá o StackOverflow quando estiver parado :-)