Não sou muito proficiente em Haskell, então essa pode ser uma pergunta muito fácil.
Que limitação de idioma o Rank2Types resolve? As funções em Haskell já não suportam argumentos polimórficos?
haskell
types
polymorphism
higher-rank-types
Andrey Shchekin
fonte
fonte
Respostas:
Eles fazem isso, mas apenas na classificação 1. Isso significa que, embora você possa escrever uma função que receba diferentes tipos de argumentos sem essa extensão, não pode escrever uma função que use seu argumento como tipos diferentes na mesma invocação.
Por exemplo, a seguinte função não pode ser digitada sem esta extensão porque
g
é usada com diferentes tipos de argumento na definição def
:Observe que é perfeitamente possível passar uma função polimórfica como um argumento para outra função. Portanto, algo como
map id ["a","b","c"]
é perfeitamente legal. Mas a função só pode usá-lo como monomórfico. No exemplomap
usaid
como se tivesse tipoString -> String
. E, claro, você também pode passar uma função monomórfica simples do tipo fornecido em vez deid
. Sem rank2types, não há como uma função exigir que seu argumento seja uma função polimórfica e, portanto, também não há como usá-lo como uma função polimórfica.fonte
f' g x y = g x + g y
. Seu tipo inferido de classificação 1 éforall a r. Num r => (a -> r) -> a -> a -> r
. Comoforall a
está fora das setas de função, o chamador deve primeiro escolher um tipo paraa
; se eles escolheremInt
, obtemosf' :: forall r. Num r => (Int -> r) -> Int -> Int -> r
, e agora corrigimos og
argumento para que ele possa aceitar,Int
mas nãoString
. Se habilitarmosRankNTypes
, podemos anotarf'
com tipoforall b c r. Num r => (forall a. a -> r) -> b -> c -> r
. Mas não posso usar - o que seriag
?É difícil entender o polimorfismo de classificação superior, a menos que você estude o Sistema F diretamente, porque Haskell foi projetado para esconder de você os detalhes no interesse da simplicidade.
Mas basicamente, a ideia geral é que os tipos polimórficos não têm realmente a
a -> b
forma que têm em Haskell; na realidade, são assim, sempre com quantificadores explícitos:Se você não souber o símbolo "∀", ele será lido como "para todos";
∀x.dog(x)
significa "para todo x, x é um cachorro". "Λ" é lambda maiúsculo, usado para abstrair os parâmetros de tipo; o que a segunda linha diz é que id é uma função que recebe um tipot
e, em seguida, retorna uma função parametrizada por esse tipo.Veja, no Sistema F, você não pode simplesmente aplicar uma função como essa
id
a um valor imediatamente; primeiro você precisa aplicar a função Λ a um tipo para obter uma função λ que você aplica a um valor. Então, por exemplo:Haskell padrão (ou seja, Haskell 98 e 2010) simplifica isso para você por não ter nenhum desses quantificadores de tipo, lambdas capitais e aplicativos de tipo, mas nos bastidores o GHC os coloca quando analisa o programa para compilação. (Isso é tudo em tempo de compilação, eu acredito, sem sobrecarga de tempo de execução.)
Mas o tratamento automático de Haskell significa que ele assume que "∀" nunca aparece no ramo esquerdo de um tipo de função ("→").
Rank2Types
eRankNTypes
desative essas restrições e permita que você substitua as regras padrão de Haskell para onde inserirforall
.Por que você quer fazer isso? Porque o System F completo e irrestrito é muito poderoso e pode fazer muitas coisas legais. Por exemplo, ocultação de tipo e modularidade podem ser implementadas usando tipos de classificação superior. Tome por exemplo uma função simples e antiga do seguinte tipo de classificação 1 (para definir o cenário):
Para usar
f
, o chamador primeiro deve escolher quais tipos usar parar
ea
, em seguida, fornecer um argumento do tipo resultante. Então você pode escolherr = Int
ea = String
:Mas agora compare isso com o seguinte tipo de classificação superior:
Como funciona uma função desse tipo? Bem, para usá-lo, primeiro você especifica para qual tipo usar
r
. Digamos que escolhemosInt
:Mas agora
∀a
está dentro da seta de função, então você não pode escolher que tipo usara
; você deve aplicarf' Int
a uma função Λ do tipo apropriado. Isso significa que a implementação def'
consegue escolher o tipo de usoa
, não o chamador def'
. Sem tipos de classificação superior, ao contrário, o chamador sempre escolhe os tipos.Para que isso é útil? Bem, para muitas coisas, na verdade, mas uma ideia é que você pode usar isso para modelar coisas como programação orientada a objetos, onde "objetos" empacotam alguns dados ocultos junto com alguns métodos que funcionam nos dados ocultos. Portanto, por exemplo, um objeto com dois métodos - um que retorna um
Int
e outro que retorna aString
, pode ser implementado com este tipo:Como é que isso funciona? O objeto é implementado como uma função que possui alguns dados internos de tipo oculto
a
. Para realmente usar o objeto, seus clientes passam uma função de "retorno de chamada" que o objeto chamará com os dois métodos. Por exemplo:Aqui estamos, basicamente, invocando o segundo método do objeto, aquele cujo tipo é
a → String
para um desconhecidoa
. Bem, desconhecido paramyObject
os clientes de; mas esses clientes sabem, pela assinatura, que poderão aplicar qualquer uma das duas funções a ela e obter umInt
ou aString
.Para um exemplo real de Haskell, abaixo está o código que escrevi quando aprendi sozinho
RankNTypes
. Isso implementa um tipo chamadoShowBox
que agrupa um valor de algum tipo oculto junto com suaShow
instância de classe. Observe que no exemplo abaixo, faço uma lista deShowBox
cujo primeiro elemento foi feito de um número e o segundo de uma string. Uma vez que os tipos são ocultados usando os tipos de classificação superior, isso não viola a verificação de tipo.PS: para quem está lendo isso e quer saber como o
ExistentialTypes
GHC usaforall
, acredito que a razão é porque ele está usando esse tipo de técnica nos bastidores.fonte
exists
palavra-chave, poderia definir um tipo existencial como (por exemplo)data Any = Any (exists a. a)
, ondeAny :: (exists a. a) -> Any
. Usando ∀xP (x) → Q ≡ (∃xP (x)) → Q, podemos concluir queAny
também poderia ter um tipoforall a. a -> Any
e é daí queforall
vem a palavra - chave. Eu acredito que os tipos existenciais implementados pelo GHC são apenas tipos de dados comuns que também carregam todos os dicionários de typeclass necessários (não consegui encontrar uma referência para fazer backup disso, desculpe).data ApplyBox r = forall a. ApplyBox (a -> r) a
; quando você corresponde a padrãoApplyBox f x
, obtémf :: h -> r
ex :: h
para um tipo restrito "oculto"h
. Se eu entendi direito, o caso do dicionário de typeclass é traduzido em algo assim:data ShowBox = forall a. Show a => ShowBox a
é traduzido em algo comodata ShowBox' = forall a. ShowBox' (ShowDict' a) a
;instance Show ShowBox' where show (ShowBox' dict val) = show' dict val
;show' :: ShowDict a -> a -> String
.A resposta de Luis Casillas fornece muitas informações excelentes sobre o que significam os tipos de classificação 2, mas vou apenas expandir um ponto que ele não cobriu. Exigir que um argumento seja polimórfico não permite apenas que ele seja usado com vários tipos; também restringe o que aquela função pode fazer com seu (s) argumento (s) e como pode produzir seu resultado. Ou seja, dá menos ao chamador flexibilidade . Por que você gostaria de fazer isso? Vou começar com um exemplo simples:
Suponha que temos um tipo de dados
e queremos escrever uma função
que assume uma função que deve escolher um dos elementos da lista fornecida e retornar uma
IO
ação de lançamento de mísseis naquele alvo. Poderíamos darf
um tipo simples:O problema é que podemos executar acidentalmente
e então estaríamos em apuros! Dando
f
um tipo polimórfico de classificação 1não ajuda em nada, porque escolhemos o tipo
a
quando ligamosf
, e apenas nos especializamosCountry
e usamos o nosso malicioso\_ -> BestAlly
novamente. A solução é usar um tipo de classificação 2:Agora, a função que passamos deve ser polimórfica, portanto
\_ -> BestAlly
, não digite check! Na verdade, nenhuma função que retorne um elemento que não esteja na lista fornecida irá typecheck (embora algumas funções que entram em loops infinitos ou produzam erros e, portanto, nunca retornem o farão).O que foi dito acima é planejado, é claro, mas uma variação dessa técnica é a chave para tornar a
ST
mônada segura.fonte
Os tipos de classificação mais alta não são tão exóticos quanto as outras respostas parecem. Acredite ou não, muitas linguagens orientadas a objetos (incluindo Java e C #!) Os apresentam. (Claro, ninguém nessas comunidades os conhece pelo nome assustador de "tipos de classificação superior".)
O exemplo que vou dar é uma implementação de livro do padrão Visitor, que uso o tempo todo em meu trabalho diário. Esta resposta não pretende ser uma introdução ao padrão do visitante; esse conhecimento está prontamente disponível em outro lugar .
Nesta aplicação de RH imaginária idiota, desejamos operar com funcionários que podem ser funcionários permanentes em tempo integral ou contratados temporários. Minha variante preferida do padrão Visitor (e de fato aquela que é relevante
RankNTypes
) parametriza o tipo de retorno do visitante.A questão é que vários visitantes com diferentes tipos de retorno podem operar com os mesmos dados. Este meio não
IEmployee
deve expressar nenhuma opinião sobre o queT
deveria ser.Desejo chamar sua atenção para os tipos. Observe que
IEmployeeVisitor
quantifica universalmente seu tipo de retorno, enquantoIEmployee
quantifica-o dentro de seuAccept
método - ou seja, em um posto superior. Traduzindo desajeitadamente de C # para Haskell:Então aí está. Tipos de classificação superior aparecem em C # quando você escreve tipos que contêm métodos genéricos.
fonte
Os slides do curso Haskell de Bryan O'Sullivan em Stanford me ajudaram a entender
Rank2Types
.fonte
Para aqueles familiarizados com as linguagens orientadas a objetos, uma função de classificação superior é simplesmente uma função genérica que espera como seu argumento outra função genérica.
Por exemplo, em TypeScript você pode escrever:
Veja como o tipo de função genérica
Identify
exige uma função genérica do tipoIdentifier
? Isso tornaIdentify
uma função de classificação superior.fonte
Accept
tem um tipo polimórfico de classificação 1, mas é um método deIEmployee
, que é ele próprio classificação 2. Se alguém me der umIEmployee
, posso abri-lo e usar seuAccept
método em qualquer tipo.Visitee
classe que você apresenta. Uma funçãof :: Visitee e => T e
é (uma vez que o material da classe é removido) essencialmentef :: (forall r. e -> Visitor e r -> r) -> T e
. Haskell 2010 permite que você fuja com polimorfismo de classificação 2 limitado usando classes como essa.forall
no meu exemplo. Não tenho uma referência de improviso, mas você pode muito bem encontrar algo em "Eliminar suas classes de tipo" . O polimorfismo de classificação superior pode de fato introduzir problemas de verificação de tipo, mas a classificação limitada implícita no sistema de classes é adequada.