Um tipo inferior é um construto que aparece principalmente na teoria matemática dos tipos. Também é chamado de tipo vazio. É um tipo que não tem valores, mas é um subtipo de todos os tipos.
Se o tipo de retorno de uma função é o tipo inferior, isso significa que ele não retorna. Período. Talvez ele faça um loop para sempre, ou talvez gere uma exceção.
Qual o sentido de ter esse tipo estranho em uma linguagem de programação? Não é tão comum, mas está presente em alguns, como Scala e Lisp.
programming-languages
type-systems
GregRos
fonte
fonte
void
dados ...void
e o tipo de unidade deve ter um valor. Além disso, como você apontou, você não pode nem mesmo declarar um valor do tipovoid
, o que significa que não é nem um tipo, apenas uma caixa de canto especial no idioma.void
em Java é quase o mesmo: não é realmente um tipo e não pode ter valores.nil
(aka,()
), que é um tipo de unidade.Respostas:
Vou dar um exemplo simples: C ++ vs Rust.
Aqui está uma função usada para lançar uma exceção no C ++ 11:
E aqui está o equivalente em Rust:
Em uma questão puramente sintática, o construto Rust é mais sensível. Observe que a construção C ++ especifica um tipo de retorno , embora também especifique que não retornará. Isso é um pouco estranho.
Em uma nota padrão, a sintaxe do C ++ apareceu apenas com o C ++ 11 (foi abordada na parte superior), mas vários compiladores vinham fornecendo várias extensões há algum tempo, de modo que ferramentas de análise de terceiros precisavam ser programadas para reconhecer as várias maneiras esse atributo pode ser gravado. Tê-lo padronizado é obviamente claramente superior.
Agora, quanto ao benefício?
O fato de uma função não retornar pode ser útil para:
fonte
void
no seu exemplo C ++ define (parte de) o tipo da função - não o tipo de retorno. Ele restringe o valor que a função tem permissãoreturn
; qualquer coisa que possa ser convertida em nula (que não é nada). Se a funçãoreturn
s não deve ser seguida por um valor. O tipo integral da função évoid () (char const*, char const*, int, char const *)
. + 1 para usar emchar const
vez deconst char
:-) #[[noreturn]]
par da sintaxe ou uma adição de funcionalidade?A resposta de Karl é boa. Aqui está um uso adicional que acho que ninguém mais mencionou. O tipo de
deve ser um tipo que inclua todos os valores no tipo de
A
e todos os valores no tipo deB
. Se o tipo deB
forNothing
, o tipo daif
expressão pode ser o tipo deA
. Eu frequentemente declaro uma rotinadizer que não se espera que o código seja alcançado. Como seu tipo é
Nothing
,unreachable(s)
agora pode ser usado em qualquer umif
(ou mais frequentemente)switch
sem afetar o tipo de resultado. Por exemploScala tem esse tipo de Nada.
Outro caso de uso para
Nothing
(como mencionado na resposta de Karl) é List [Nothing] é o tipo de lista em que cada membro tem o tipo Nothing. Assim, pode ser o tipo da lista vazia.A propriedade-chave
Nothing
que faz com que esses casos de uso funcionem não é que não tenha valores - embora no Scala, por exemplo, não tenha valores - é que é um subtipo de qualquer outro tipo.Suponha que você tenha um idioma em que cada tipo contenha o mesmo valor - vamos chamá-lo
()
. Nesse idioma, o tipo de unidade, que tem()
como único valor, pode ser um subtipo de todo tipo. Isso não o torna um tipo de base no sentido que o OP significava; o OP ficou claro que um tipo inferior não contém valores. No entanto, como é um tipo que é um subtipo de todo tipo, pode desempenhar o mesmo papel que um tipo inferior.Haskell faz as coisas de maneira um pouco diferente. Em Haskell, uma expressão que nunca produz um valor pode ter o esquema de tipos
forall a.a
. Uma instância desse esquema de tipo será unificada com qualquer outro tipo, portanto, atua efetivamente como um tipo inferior, mesmo que Haskell (padrão) não tenha noção de subtipagem. Por exemplo, aerror
função do prelúdio padrão possui esquema de tiposforall a. [Char] -> a
. Então você pode escrevere o tipo da expressão será o mesmo que o tipo de
A
, para qualquer expressãoA
.A lista vazia em Haskell possui o esquema de tipos
forall a. [a]
. SeA
é uma expressão cujo tipo é um tipo de lista, entãoé uma expressão do mesmo tipo que
A
.fonte
forall a . [a]
e o tipo[a]
em Haskell? As variáveis de tipo já não são quantificadas universalmente nas expressões de tipo Haskell?forall
no padrão Haskell 2010. Eu escrevi a quantificação explicitamente porque este não é um fórum do Haskell e algumas pessoas podem não estar familiarizadas com as convenções de Haskell. Portanto, não há diferença, exceto que issoforall a . [a]
não é padrão, enquanto[a]
é.Os tipos formam um monóide de duas maneiras, juntos formando um semicondutor . É o que se chama tipos de dados algébricos . Para tipos finitos, essa semicondução está diretamente relacionada à semântica de números naturais (incluindo zero), o que significa que você conta quantos valores possíveis o tipo possui (excluindo "valores não-determinantes").
Vacuous
) tem zero valores † .()
.(Bool, Bool)
tem quatro valores possíveis, nomeadamente(False,False)
,(False,True)
,(True,False)
e(True,True)
.O tipo de unidade é o elemento de identidade da operação de composição. Por exemplo,
((), False)
e((), True)
são os únicos valores do tipo((), Bool)
, portanto esse tipo é isomórfico paraBool
si mesmo.A
eB
basicamente tem todos os valores deA
, mais todos os valores deB
, portanto, o tipo de soma . Por exemplo,Either () Bool
tem três valores, eu vou chamá-losLeft ()
,Right False
eRight True
.O tipo inferior é o elemento de identidade da soma:
Either Vacuous A
possui apenas valores do formulárioRight a
, porqueLeft ...
não faz sentido (Vacuous
não possui valores).O interessante desses monóides é que, quando você introduz funções na sua linguagem, a categoria desses tipos com as funções como morfismos é uma categoria monoidal . Entre outras coisas, isso permite definir functores e mônadas aplicáveis , que se tornam uma excelente abstração para cálculos gerais (possivelmente envolvendo efeitos colaterais etc.) em termos puramente funcionais.
Agora, na verdade, você pode ir muito longe preocupando apenas um lado do problema (a composição monóide), e não precisa realmente do tipo de fundo explicitamente. Por exemplo, até Haskell, por um longo tempo, não tinha um tipo inferior padrão. Agora tem, é chamado
Void
.Mas quando você considera a imagem completa, como uma categoria fechada bicartesiana , o sistema de tipos é realmente equivalente a todo o cálculo lambda, então basicamente você tem a abstração perfeita sobre tudo o que é possível em uma linguagem completa de Turing. Ótimo para linguagens específicas de domínio incorporadas, por exemplo, existe um projeto sobre a codificação direta de circuitos eletrônicos dessa maneira .
Claro, você pode muito bem dizer que isso é um absurdo geral de todos os teóricos . Você não precisa conhecer a teoria das categorias para ser um bom programador, mas, quando o faz, oferece maneiras poderosas e ridiculamente gerais de raciocinar sobre código e provar invariantes.
† mb21 me lembra que note que isso não deve ser confundido com valores inferiores . Em idiomas preguiçosos como Haskell, todo tipo contém um "valor" inferior, denotado
⊥
. Isso não é algo concreto que você possa explicar explicitamente; é o que é "retornado", por exemplo, quando uma função faz um loop para sempre. Até oVoid
tipo de Haskell “contém” o valor inferior, assim o nome. Sob essa luz, o tipo inferior de Haskell realmente tem um valor e seu tipo de unidade tem dois valores, mas na discussão da teoria da categoria isso geralmente é ignorado.fonte
Void
)", que não deve ser confundido com o valorbottom
, que é membro de qualquer tipo em Haskell .Parece um tipo útil de ter nessas situações, por mais raros que sejam.
Além disso, mesmo que
Nothing
(o nome de Scala para o tipo inferior) não possa ter valores,List[Nothing]
não possui essa restrição, o que a torna útil como o tipo de uma lista vazia. A maioria dos idiomas contorna isso, tornando uma lista vazia de seqüências de caracteres um tipo diferente de uma lista vazia de números inteiros, o que faz sentido, mas torna uma lista vazia mais detalhada para escrever, o que é uma grande desvantagem em uma linguagem orientada a listas.fonte
[]
representam todos eles, e serão instanatiadas para o tipo específico, conforme necessário.[a]
. Da mesma forma,:t Left 1
produzNum a => Either a b
. A avaliação efetiva da expressão força o tipo dea
, mas não o deb
:Either Integer b
forall
em seu tipoforall a. [a]
,. Existem algumas maneiras legais de pensarforall
, mas leva algum tempo para realmente descobrir.*
.[]
é um construtor de tipos e[]
é uma expressão que representa uma lista vazia. Mas isso não significa que "a lista vazia de Haskell é um construtor de tipos". O contexto deixa claro se[]
está sendo usado como um tipo ou como uma expressão. Suponha que você declaredata Foo x = Foo | Bar x (Foo x)
; agora você pode usarFoo
como um construtor de tipos ou como um valor, mas é por acaso que você escolheu o mesmo nome para ambos.É útil para a análise estática documentar o fato de que um caminho de código específico não está acessível. Por exemplo, se você escrever o seguinte em C #:
O compilador reclamará que
F
não retorna nada em pelo menos um caminho de código. SeAssert
fosse marcado como não retornando, o compilador não precisaria avisar.fonte
Em algumas linguagens,
null
possui o tipo inferior, já que o subtipo de todos os tipos define bem para que linguagens usam nulo (apesar da leve contradição denull
ser ao mesmo tempo uma função que se retorna, evitando os argumentos comuns sobre por quebot
deveria ser desabitado).Também pode ser usado como uma função geral nos tipos (
any -> bot
) para lidar com o despacho que deu errado.E alguns idiomas permitem que você realmente resolva
bot
como um erro, que pode ser usado para fornecer erros de compilador personalizados.fonte
void
idiomas comuns (embora com semântica ligeiramente diferente para o mesmo uso), nãonull
. Embora você também esteja certo, a maioria dos idiomas não modela nulo como o tipo inferior.null
, por exemplo, você pode comparar um ponteiro comnull
um resultado booleano. Eu acho que as respostas estão mostrando que existem dois tipos distintos de tipos inferiores. (a) Idiomas (por exemplo, Scala), em que o tipo que é um subtipo de todo tipo representa cálculos que não fornecem nenhum resultado. Essencialmente, é um tipo vazio, embora tecnicamente frequentemente seja preenchido por um valor inferior inútil, que representa não terminação. (b) Idiomas como Tangent, em que o tipo inferior é um subconjunto de qualquer outro tipo, pois contém um valor útil que também é encontrado em todos os outros tipos - nulo.Sim, este é um tipo bastante útil; embora seu papel seja principalmente interno ao sistema de tipos, há algumas ocasiões em que o tipo inferior apareceria abertamente.
Considere uma linguagem de tipo estaticamente em que condicionais são expressões (para que a construção if-then-else dobre como o operador ternário de C e amigos, e pode haver uma declaração de caso de várias maneiras semelhante). A linguagem de programação funcional possui isso, mas isso também acontece em certas linguagens imperativas (desde o ALGOL 60). Então todas as expressões de ramificação devem finalmente produzir o tipo de toda a expressão condicional. Pode-se simplesmente exigir que seus tipos sejam iguais (e acho que esse é o caso do operador ternário em C), mas isso é excessivamente restritivo, especialmente quando o condicional também pode ser usado como declaração condicional (sem retornar nenhum valor útil). Em geral, deseja-se que cada expressão de ramificação seja (implicitamente) conversível para um tipo comum que será o tipo da expressão completa (possivelmente com restrições mais ou menos complicadas para permitir que esse tipo comum seja efetivamente encontrado pelo complier, cf. C ++, mas não abordarei esses detalhes aqui).
Existem dois tipos de situações em que um tipo geral de conversão permitirá a flexibilidade necessária de tais expressões condicionais. Um já foi mencionado, em que o tipo de resultado é o tipo de unidade
void
; esse é naturalmente um supertipo de todos os outros tipos e, ao permitir que qualquer tipo seja (trivialmente) convertido, é possível usar a expressão condicional como instrução condicional. O outro envolve casos em que a expressão retorna um valor útil, mas uma ou mais ramificações são incapazes de produzir uma. Eles geralmente geram uma exceção ou envolvem um salto, e exigir que eles (também) produzam um valor do tipo de toda a expressão (de um ponto inacessível) seria inútil. É esse tipo de situação que pode ser tratada com clareza, fornecendo cláusulas, saltos e chamadas para aumento de exceção que terão esse efeito, o tipo inferior, o tipo que pode ser (trivialmente) convertido em qualquer outro tipo.Eu sugeriria escrever um tipo inferior que
*
sugerisse sua conversibilidade para um tipo arbitrário. Pode servir a outros propósitos úteis internamente, por exemplo, ao tentar deduzir um tipo de resultado para uma função recursiva que não declara nenhum, o inferenciador de tipo pode atribuir o tipo*
a qualquer chamada recursiva para evitar uma situação de galinha e ovo; o tipo real será determinado por ramificações não recursivas e as recursivas serão convertidas no tipo comum das não recursivas. Se não houver ramificações não recursivas, o tipo permanecerá*
e indicará corretamente que a função não tem como retornar da recursão. Além disso, e como tipo de resultado das funções de lançamento de exceção, pode-se usar*
como tipo de componente de sequências de comprimento 0, por exemplo da lista vazia; novamente se um elemento for selecionado a partir de uma expressão do tipo[*]
(lista necessariamente vazia), o tipo resultante*
indicará corretamente que isso nunca poderá retornar sem erro.fonte
var foo = someCondition() ? functionReturningBar() : functionThatAlwaysThrows()
poderia inferir o tipo defoo
asBar
, já que a expressão nunca poderia produzir mais nada?void
em C. A segunda parte da sua resposta, em que você fala sobre um tipo para uma função que nunca retorna, ou uma lista sem elementos - isso é realmente o tipo de baixo! (É muitas vezes escrito como_|_
em vez de*
Não sei por que Talvez porque ele se parece com um (fundo humano) :)..functionThatAlwaysThrows()
foram substituídos por um explícitothrow
, devido à linguagem especial na norma. Ter um tipo que faça isso seria uma melhoria.Em alguns idiomas, você pode anotar uma função para informar ao compilador e aos desenvolvedores que uma chamada para essa função não retornará (e se a função for escrita de uma maneira que possa retornar, o compilador não permitirá ) É uma coisa útil a saber, mas no final você pode chamar uma função como essa como qualquer outra. O compilador pode usar as informações para otimização, para fornecer avisos sobre código morto e assim por diante. Portanto, não há uma razão muito convincente para ter esse tipo, mas também não há uma razão muito convincente para evitá-lo.
Em muitos idiomas, uma função pode retornar "nula". O que isso significa exatamente depende do idioma. Em C, significa que a função não retorna nada. No Swift, isso significa que a função retorna um objeto com apenas um valor possível e, como existe apenas um valor possível, esse valor recebe zero bits e não exige nenhum código. Em ambos os casos, isso não é o mesmo que "inferior".
"bottom" seria um tipo sem valores possíveis. Isso nunca pode existir. Se uma função retornar "bottom", na verdade não poderá retornar, porque não há valor do tipo "bottom" que possa retornar.
Se um designer de linguagem quiser, não há motivo para não ter esse tipo. A implementação não é difícil (você pode implementá-la exatamente como uma função retornando nula e marcada como "não retorna"). Você não pode misturar ponteiros para funções retornando inferior com ponteiros para funções retornando nulos, porque eles não são do mesmo tipo).
fonte