Un tipo de fondo es una construcción que aparece principalmente en la teoría de tipos matemáticos. También se llama el tipo vacío. Es un tipo que no tiene valores, pero es un subtipo de todos los tipos.
Si el tipo de retorno de una función es el tipo inferior, eso significa que no regresa. Período. Tal vez se repita para siempre, o tal vez arroje una excepción.
¿Cuál es el punto de tener este tipo extraño en un lenguaje de programación? No es tan común, pero está presente en algunos, como Scala y Lisp.
programming-languages
type-systems
GregRos
fuente
fuente
void
dato ...void
, y el tipo de unidad debe tener un valor. Además, como señaló, ni siquiera puede declarar un valor de tipovoid
, eso significa que ni siquiera es un tipo, solo un caso de esquina especial en el idioma.void
en Java es casi lo mismo: no es realmente un tipo y no puede tener valores.nil
(también conocido como()
), que es un tipo de unidad.Respuestas:
Tomaré un ejemplo simple: C ++ vs Rust.
Aquí hay una función utilizada para lanzar una excepción en C ++ 11:
Y aquí está el equivalente en Rust:
En un asunto puramente sintáctico, la construcción Rust es más sensata. Tenga en cuenta que la construcción C ++ especifica un tipo de retorno , aunque también especifica que no va a volver. Eso es un poco raro.
En una nota estándar, la sintaxis de C ++ solo apareció con C ++ 11 (se agregó en la parte superior), pero varios compiladores habían estado proporcionando varias extensiones durante un tiempo, por lo que las herramientas de análisis de terceros tuvieron que programarse para reconocer las diversas formas Este atributo podría ser escrito. Tenerlo estandarizado es obviamente claramente superior.
Ahora, en cuanto al beneficio?
El hecho de que una función no regrese puede ser útil para:
fuente
void
en su ejemplo de C ++ define (parte de) el tipo de función, no el tipo de retorno. Restringe el valor al que se permite la funciónreturn
; cualquier cosa que se pueda convertir a vacío (que no es nada). Si la funciónreturn
s no debe ir seguida de un valor. El tipo completo de la función esvoid () (char const*, char const*, int, char const *)
. + 1 para usar enchar const
lugar deconst char
:-)[[noreturn]]
de la sintaxis o una adición de funcionalidad?La respuesta de Karl es buena. Aquí hay un uso adicional que no creo que nadie más haya mencionado. El tipo de
debe ser un tipo que incluya todos los valores en el tipo de
A
y todos los valores en el tipo deB
. Si el tipo deB
esNothing
, entonces el tipo de laif
expresión puede ser el tipo deA
. A menudo declaro una rutinadecir que no se espera alcanzar el código. Dado que su tipo es
Nothing
,unreachable(s)
ahora se puede usar en cualquieraif
o (más a menudo)switch
sin afectar el tipo de resultado. Por ejemploScala tiene ese tipo de Nada.
Otro caso de uso para
Nothing
(como se menciona en la respuesta de Karl) es Lista [Nada] es el tipo de listas de cada uno de cuyos miembros tiene el tipo Nada. Por lo tanto, puede ser el tipo de la lista vacía.La propiedad clave de
Nothing
esto hace que estos casos de uso funcionen no es que no tenga valores, aunque en Scala, por ejemplo, no tiene valores, es que es un subtipo de cualquier otro tipo.Supongamos que tiene un idioma donde cada tipo contiene el mismo valor, llamémoslo
()
. En dicho lenguaje, el tipo de unidad, que tiene()
como único valor, podría ser un subtipo de cada tipo. Eso no lo convierte en un tipo inferior en el sentido que significaba el OP; el OP dejó claro que un tipo de fondo no contiene valores. Sin embargo, como es un tipo que es un subtipo de cada tipo, puede desempeñar el mismo papel que un tipo inferior.Haskell hace las cosas un poco diferente. En Haskell, una expresión que nunca produce un valor puede tener el esquema de tipo
forall a.a
. Una instancia de este esquema de tipo se unificará con cualquier otro tipo, por lo que efectivamente actúa como un tipo de fondo, aunque Haskell (estándar) no tiene noción de subtipo. Por ejemplo, laerror
función del preludio estándar tiene un esquema de tipoforall a. [Char] -> a
. Entonces puedes escribiry el tipo de expresión será el mismo que el tipo de
A
, para cualquier expresiónA
.La lista vacía en Haskell tiene el esquema de tipo
forall a. [a]
. SiA
es una expresión cuyo tipo es un tipo de lista, entonceses una expresión con el mismo tipo que
A
.fuente
forall a . [a]
y el tipo[a]
en Haskell? ¿Las variables de tipo no están cuantificadas universalmente en las expresiones de tipo Haskell?forall
en el estándar Haskell 2010. Escribí la cuantificación explícitamente porque este no es un foro de Haskell y algunas personas podrían no estar familiarizadas con las convenciones de Haskell. Por lo tanto, no hay diferencia, excepto queforall a . [a]
no es estándar, mientras que lo[a]
es.Los tipos forman un monoide de dos maneras, juntos haciendo un semired . Eso es lo que se llama tipos de datos algebraicos . Para los tipos finitos, este semiring se relaciona directamente con el semired de los números naturales (incluido el cero), lo que significa que cuenta cuántos valores posibles tiene el tipo (excluyendo los "valores no determinantes").
Vacuous
) tiene valores cero † .()
.(Bool, Bool)
tiene cuatro valores posibles, a saber(False,False)
,(False,True)
,(True,False)
y(True,True)
.El tipo de unidad es el elemento de identidad de la operación de composición. Por ejemplo,
((), False)
y((), True)
son los únicos valores de tipo((), Bool)
, por lo que este tipo es isomorfo aBool
sí mismo.A
yB
básicamente tiene todos los valores deA
, además de todos los valores deB
, por lo tanto, el tipo de suma . Por ejemplo,Either () Bool
tiene tres valores, voy a llamarlosLeft ()
,Right False
yRight True
.El tipo inferior es el elemento de identidad de la suma: solo
Either Vacuous A
tiene valores de la forma , porque no tiene sentido ( no tiene valores).Right a
Left ...
Vacuous
Lo interesante de estos monoides es que, cuando introduce funciones en su idioma, la categoría de estos tipos con las funciones de morfismos es una categoría monoidal . Entre otras cosas, esto le permite definir functores y mónadas aplicativos , que resultan ser una excelente abstracción para los cálculos generales (posiblemente con efectos secundarios, etc.) en términos puramente funcionales.
Ahora, en realidad puede llegar bastante lejos si se preocupa solo por un lado del problema (el monoide de composición), entonces realmente no necesita el tipo de fondo explícitamente. Por ejemplo, incluso Haskell no tuvo durante mucho tiempo un tipo de fondo estándar. Ahora sí, se llama
Void
.Pero cuando considera la imagen completa, como una categoría cerrada bicartesiana , el sistema de tipos es realmente equivalente a todo el cálculo lambda, por lo que básicamente tiene la abstracción perfecta sobre todo lo posible en un lenguaje completo de Turing. Ideal para lenguajes integrados específicos de dominio, por ejemplo, hay un proyecto sobre codificar directamente circuitos electrónicos de esta manera .
Por supuesto, bien puedes decir que esto es una tontería general de todos los teóricos . No es necesario que conozca la teoría de categorías para ser un buen programador, pero cuando lo hace, le brinda formas poderosas y ridículamente generales de razonar sobre el código y probar invariantes.
† mb21 me recuerda que tenga en cuenta que esto no debe confundirse con los valores inferiores . En lenguajes perezosos como Haskell, cada tipo contiene un "valor" inferior, denotado
⊥
. Esto no es algo concreto que puedas pasar explícitamente, sino que es lo que se "devuelve", por ejemplo, cuando una función se repite para siempre. Incluso elVoid
tipo de Haskell "contiene" el valor inferior, de ahí el nombre. Desde ese punto de vista, el tipo de fondo de Haskell realmente tiene un valor y su tipo de unidad tiene dos valores, pero en la discusión de teoría de categorías esto generalmente se ignora.fuente
Void
)", que no debe confundirse con el valorbottom
, que es miembro de cualquier tipo en Haskell .Suena como un tipo útil para tener en esas situaciones, aunque sean raros.
Además, aunque
Nothing
(el nombre de Scala para el tipo inferior) no puede tener valores,List[Nothing]
no tiene esa restricción, lo que lo hace útil como el tipo de una lista vacía. La mayoría de los idiomas resuelven esto haciendo una lista vacía de cadenas de un tipo diferente que una lista vacía de enteros, lo que tiene sentido, pero hace que una lista vacía sea más detallada para escribir, lo cual es un gran inconveniente en un lenguaje orientado a listas.fuente
[]
representa a todos y se instan El tipo específico según sea necesario.[a]
. Del mismo modo, los:t Left 1
rendimientosNum a => Either a b
. En realidad, evaluar la expresión fuerza el tipo dea
, pero no deb
:Either Integer b
forall
en su tipoforall a. [a]
,. Hay algunas maneras agradables de pensarforall
, pero lleva un tiempo darse cuenta realmente.*
.[]
es un constructor de tipos y[]
es una expresión que representa una lista vacía. Pero eso no significa que "la lista vacía de Haskell sea un constructor de tipos". El contexto deja en claro si[]
se está utilizando como tipo o como expresión. Supongamos que declarasdata Foo x = Foo | Bar x (Foo x)
; ahora puede usarloFoo
como un constructor de tipos o como un valor, pero es casualidad que haya elegido el mismo nombre para ambos.Es útil para el análisis estático documentar el hecho de que una ruta de código particular no es accesible. Por ejemplo, si escribe lo siguiente en C #:
El compilador se quejará de que
F
no devuelve nada en al menos una ruta de código. SiAssert
se marcara como no devuelto, el compilador no necesitaría avisar.fuente
En algunos idiomas,
null
tiene el tipo inferior, ya que el subtipo de todos los tipos define muy bien para qué lenguajes usan nulo (a pesar de la leve contradicción denull
ser ambos mismos y una función que se devuelve, evitando los argumentos comunes sobre por québot
debería estar deshabitada).También se puede usar como una función general en los tipos de función (
any -> bot
) para manejar el envío que salió mal.Y algunos idiomas le permiten resolverlo realmente
bot
como un error, que puede usarse para proporcionar errores de compilación personalizados.fuente
void
idiomas comunes (aunque con semánticas ligeramente diferentes para el mismo uso), nonull
. Aunque también tiene razón en que la mayoría de los idiomas no modelan nulo como el tipo inferior.null
, por ejemplo, comparar un puntero paranull
obtener un resultado booleano. Creo que las respuestas muestran que hay dos tipos distintos de tipos inferiores. (a) Idiomas (p. ej., Scala) donde el tipo que es un subtipo de cada tipo representa cálculos que no ofrecen ningún resultado. Esencialmente es un tipo vacío, aunque técnicamente a menudo está poblado por un valor inferior inútil que representa la no determinación. (b) Idiomas como Tangente, en el que el tipo inferior es un subconjunto de cualquier otro tipo porque contiene un valor útil que también se encuentra en cualquier otro tipo: nulo.Sí, este es un tipo bastante útil; Si bien su función sería principalmente interior al sistema de tipos, hay algunas ocasiones en las que el tipo de fondo aparecería abiertamente.
Considere un lenguaje de tipo estático en el que los condicionales son expresiones (por lo que la construcción if-then-else se duplica como el operador ternario de C y amigos, y puede haber una declaración de caso de múltiples vías similar). El lenguaje de programación funcional tiene esto, pero también ocurre en ciertos lenguajes imperativos (desde ALGOL 60). Entonces, todas las expresiones de rama deben producir finalmente el tipo de la expresión condicional completa. Uno podría simplemente requerir que sus tipos sean iguales (y creo que este es el caso del operador ternario en C), pero esto es demasiado restrictivo, especialmente cuando el condicional también se puede usar como enunciado condicional (sin devolver ningún valor útil). En general, se quiere que cada expresión de rama sea (implícitamente) convertible a un tipo común que será el tipo de la expresión completa (posiblemente con restricciones más o menos complicadas para permitir que el compilador encuentre efectivamente ese tipo común, cf. C ++, pero no entraré en esos detalles aquí).
Hay dos tipos de situaciones en las que un tipo general de conversión permitirá la flexibilidad necesaria de tales expresiones condicionales. Ya se mencionó uno, donde el tipo de resultado es el tipo de unidad
void
; Esto es naturalmente un supertipo de todos los otros tipos, y permitir que cualquier tipo se convierta (trivialmente) en él hace posible usar la expresión condicional como declaración condicional. El otro involucra casos en los que la expresión devuelve un valor útil, pero una o más ramas son incapaces de producir uno. Por lo general, plantearán una excepción o implicarán un salto, y requerirles que (también) produzcan un valor del tipo de la expresión completa (desde un punto inalcanzable) no tendría sentido. Es este tipo de situación la que se puede manejar con gracia al dar cláusulas, saltos y llamadas de aumento de excepciones que tendrán ese efecto, el tipo inferior, el único tipo que puede convertirse (trivialmente) en cualquier otro tipo.Sugeriría escribir un tipo inferior
*
para sugerir su convertibilidad a tipo arbitrario. Puede tener otros propósitos útiles internamente, por ejemplo, cuando se intenta deducir un tipo de resultado para una función recursiva que no declara ninguno, el inferenciador de tipo podría asignar el tipo*
a cualquier llamada recursiva para evitar una situación de gallina y huevo; el tipo real estará determinado por ramas no recursivas, y las recursivas se convertirán al tipo común de las no recursivas. Si no hay ramas no recursivas, el tipo permanecerá*
e indicará correctamente que la función no tiene forma alguna de regresar de la recursividad. Aparte de esto y como resultado de las funciones de lanzamiento de excepciones, se puede usar*
como tipo de componente de secuencias de longitud 0, por ejemplo de la lista vacía; nuevamente, si alguna vez se selecciona un elemento de una expresión de tipo[*]
(lista necesariamente vacía), el tipo resultante*
indicará correctamente que esto nunca puede regresar sin un error.fuente
var foo = someCondition() ? functionReturningBar() : functionThatAlwaysThrows()
podría inferir el tipo defoo
asBar
, ya que la expresión nunca podría producir nada más?void
en C. La segunda parte de su respuesta, donde habla de un tipo para una función que nunca regresa, o una lista sin elementos, eso es realmente el tipo de fondo! (A menudo se escribe como en_|_
lugar de*
. No estoy seguro de por qué. Quizás porque parece un fondo (humano) :)functionThatAlwaysThrows()
fueron reemplazados por una explícitathrow
, debido al lenguaje especial en la Norma. Tener un tipo que haga esto sería una mejora.En algunos idiomas, puede anotar una función para decirle tanto al compilador como a los desarrolladores que una llamada a esta función no se devolverá (y si la función está escrita de una manera que pueda regresar, el compilador no lo permitirá ) Es algo útil de saber, pero al final puede llamar a una función como esta como cualquier otra. El compilador puede usar la información para la optimización, para dar advertencias sobre el código muerto, etc. Por lo tanto, no hay una razón muy convincente para tener este tipo, pero tampoco hay una razón muy convincente para evitarlo.
En muchos idiomas, una función puede devolver "void". Lo que eso significa exactamente depende del idioma. En C significa que la función no devuelve nada. En Swift, significa que la función devuelve un objeto con un solo valor posible, y dado que solo hay un valor posible, ese valor toma cero bits y en realidad no requiere ningún código. En cualquier caso, eso no es lo mismo que "fondo".
"bottom" sería un tipo sin valores posibles. Nunca puede existir. Si una función devuelve "bottom", en realidad no puede regresar, porque no hay ningún valor de tipo "bottom" que pueda devolver.
Si un diseñador de idiomas lo desea, entonces no hay razón para no tener ese tipo. La implementación no es difícil (puede implementarla exactamente como una función que devuelve nula y marcada como "no regresa"). No puede mezclar punteros a funciones que regresan al fondo con punteros a funciones que devuelven vacío, porque no son del mismo tipo).
fuente