O polimorfismo paramétrico de alto escalão é útil?

16

Tenho certeza de que todos estão familiarizados com métodos genéricos do formulário:

T DoSomething<T>(T item)

Essa função também é chamada de parametricamente polimórfico (PP), especificamente PP de classificação 1 .

Digamos que esse método possa ser representado usando um objeto de função do formulário:

<T> : T -> T

Ou seja, <T>significa T -> Tque ele usa um parâmetro de tipo Te significa que ele usa um parâmetro do tipo e retorna um valor do mesmo tipo.

Em seguida, o seguinte seria uma função PP de classificação 2:

(<T> : T -> T) -> int 

A função não possui parâmetros de tipo propriamente ditos, mas assume uma função que aceita um parâmetro de tipo. Você pode continuar isso iterativamente, tornando o aninhamento cada vez mais profundo, obtendo PP de classificação cada vez mais alta.

Esse recurso é realmente raro entre linguagens de programação. Mesmo Haskell não permite isso por padrão.

É útil? Pode descrever comportamentos difíceis de descrever de outra forma?

Além disso, o que significa algo ser impredicativo ? (neste contexto)

GregRos
fonte
1
Curiosamente, o TypeScript é uma linguagem mainstream com suporte completo a PP de classificação n. Por exemplo, o seguinte é um código TypeScript válido:let sdff = (g : (f : <T> (e : T) => void) => void) => {}
GregRos

Respostas:

11

Em geral, você usa polimorfismo de classificação mais alta quando deseja que o receptor selecione o valor de um parâmetro de tipo, e não o chamador . Por exemplo:

f :: (forall a. Show a => a -> Int) -> (Int, Int)
f g = (g "one", g 2)

Qualquer função gque eu passar para isso fdeve ser capaz de me fornecer Intum valor de algum tipo, onde a única coisa que se gsabe sobre esse tipo é que ele possui uma instância Show. Portanto, estes são kosher:

f (length . show)
f (const 42)

Mas estes não são:

f length
f succ

Uma aplicação particularmente útil é usar o escopo de tipos para impor o escopo de valores . Suponha que tenhamos um objeto do tipo Action<T>, representando uma ação que podemos executar para produzir um resultado do tipo T, como um futuro ou retorno de chamada.

T runAction<T>(Action<T>)

runAction :: forall a. Action a -> a

Agora, suponha que também tenhamos um Actionque possa alocar Resource<T>objetos:

Action<Resource<T>> newResource<T>(T)

newResource :: forall a. a -> Action (Resource a)

Queremos reforçar que esses recursos sejam usados apenas dentro do Actionlocal em que foram criados e não compartilhados entre ações diferentes ou execuções diferentes da mesma ação, para que as ações sejam determinísticas e repetíveis.

Podemos usar tipos de classificação mais alta para fazer isso adicionando um parâmetro Saos tipos Resourcee Action, que é totalmente abstrato - ele representa o "escopo" do Action. Agora, nossas assinaturas são:

T run<T>(<S> Action<S, T>)
Action<S, Resource<S, T>> newResource<T>(T)

runAction :: forall a. (forall s. Action s a) -> a
newResource :: forall s a. a -> Action s (Resource s a)

Agora, quando damos runActionum Action<S, T>, temos a certeza de que, como o parâmetro "scope" Sé totalmente polimórfico, ele não pode escapar do corpo de runAction- então qualquer valor de um tipo que use Scomo o Resource<S, int>mesmo não pode escapar!

(Em Haskell, isso é conhecido como STmônada, onde runActioné chamada runST, Resourceé chamada STRefe newResourceé chamada newSTRef).

Jon Purdy
fonte
A STmônada é um exemplo muito interessante. Você pode dar mais alguns exemplos de quando o polimorfismo de alto escalão seria útil?
23415 GregRos
@ GregRos: Também é útil com existenciais. No Haxl , tínhamos um tipo existencial data Fetch d = forall a. Fetch (d a) (MVar a), que é um par de uma solicitação para uma fonte de dados de um slot para armazenar o resultado. O resultado e o slot devem ter tipos correspondentes, mas esse tipo está oculto, para que você possa ter uma lista heterogênea de solicitações para a mesma fonte de dados. Agora você pode usar de maior grau de polimorfismo para escrever uma função que recupera todos os pedidos, dada uma função que obtém uma: fetch :: (forall a. d a -> IO a) -> [Fetch d] -> IO ().
Jon Purdy
8

Polimorfismo de classificação mais alta é extremamente útil. No Sistema F (o idioma principal das linguagens FP digitadas com as quais você está familiarizado), isso é essencial para admitir "codificações da Igreja digitadas", que é realmente como o Sistema F faz a programação. Sem eles, o sistema F é completamente inútil.

No Sistema F, definimos números como

Nat = forall c. (c -> c) -> c -> c

A adição tem o tipo

plus : Nat -> Nat -> Nat
plus l r = Λ t. λ (s : t -> t). λ (z : t). l s (r s z)

que é um tipo de classificação mais alta ( forall c.aparece dentro dessas setas).

Isso aparece em outros lugares também. Por exemplo, se você deseja indicar que um cálculo é um estilo de passagem de continuação adequado (google "codensity haskell"), então você faria isso como

type CPSed A = forall c. (A -> c) -> c

Mesmo falando sobre um tipo desabitado no Sistema F requer polimorfismo de classificação mais alta

type Void = forall a. a 

O longo e curto disso, escrever uma função em um sistema de tipo puro (Sistema F, CoC) requer polimorfismo de classificação mais alta se quisermos lidar com dados interessantes.

No sistema F, em particular, essas codificações precisam ser "impredicativas". Isso significa que um forall a.quantifica absolutamente todos os tipos . Isso inclui criticamente o tipo que estamos definindo. No forall a. aque apoderia realmente estar para forall a. anovamente! Em linguagens como ML, esse não é o caso, eles são considerados "predicativos", uma vez que uma variável de tipo quantifica apenas o conjunto de tipos sem quantificadores (chamados monotipos). Nossa definição de plusimpredicatividade requerida também porque instanciamos o cin l : Natto be Nat!

Finalmente, gostaria de mencionar uma última razão pela qual você gostaria tanto de impredicatividade quanto de polimorfismo de classificação mais alta, mesmo em um idioma com tipos arbitrariamente recursivos (ao contrário do Sistema F). Em Haskell, há uma mônada para efeitos chamada "mônada de thread de estado". A idéia é que a mônada de encadeamento de estado permita que você mude as coisas, mas exija que ela escape para que seu resultado não dependa de nada mutável. Isso significa que os cálculos de ST são notavelmente puros. Para impor esse requisito, usamos polimorfismo de classificação mais alta

runST :: forall a. (forall s. ST s a) -> a

Aqui, garantindo que isso aesteja fora do escopo em que apresentamos s, sabemos que isso asignifica um tipo bem formado, no qual não se baseia s. Usamos spara parameritizar todas as coisas mutáveis ​​nesse segmento de estado específico, para que saibamos que aé independente das coisas mutáveis ​​e, portanto, que nada escapa ao escopo desse STcálculo! Um exemplo maravilhoso de uso de tipos para descartar programas mal formados.

A propósito, se você estiver interessado em aprender sobre a teoria dos tipos, sugiro investir em um bom livro ou dois. É difícil aprender essas coisas em pedaços. Eu sugeriria um dos livros de Pierce ou Harper sobre a teoria da PL em geral (e alguns elementos da teoria dos tipos). O livro "Tópicos avançados em tipos e linguagens de programação" também abrange uma boa quantidade de teoria de tipos. Finalmente, "Programar na teoria dos tipos de Martin Lof" é uma exposição muito boa da teoria dos tipos intensional que Martin Lof delineou.

Daniel Gratzer
fonte
Obrigado por suas recomendações. Vou procurá-los. O tópico é realmente interessante e desejo que alguns conceitos mais avançados do sistema de tipos sejam adotados por mais linguagens de programação. Eles lhe dão um poder muito mais expressivo.
GregRos 23/03