En informática , la semántica denotacional (conocida inicialmente como semántica matemática o semántica de Scott-Strachey ) es un enfoque de formalización de los significados de los lenguajes de programación mediante la construcción de objetos matemáticos (llamados denotaciones ) que describen los significados de las expresiones de los lenguajes. Otros enfoques que proporcionan semántica formal de los lenguajes de programación incluyen la semántica axiomática y la semántica operacional .
En términos generales, la semántica denotacional se ocupa de encontrar objetos matemáticos llamados dominios que representan lo que hacen los programas. Por ejemplo, los programas (o frases de programas) podrían representarse mediante funciones parciales [1] [2] o mediante juegos [3] entre el entorno y el sistema.
Un principio importante de la semántica denotacional es que la semántica debe ser compositiva : la denotación de una frase de programa debe construirse a partir de las denotaciones de sus subfrases .
La semántica denotacional se originó en el trabajo de Christopher Strachey y Dana Scott publicado a principios de la década de 1970. [1] [2] Tal como fue desarrollada originalmente por Strachey y Scott, la semántica denotacional proporcionaba el significado de un programa informático como una función que mapeaba la entrada en la salida. [2] Para dar significados a los programas definidos recursivamente , Scott propuso trabajar con funciones continuas entre dominios , específicamente órdenes parciales completas . Como se describe a continuación, se ha continuado el trabajo en la investigación de la semántica denotacional apropiada para aspectos de los lenguajes de programación como la secuencialidad, la concurrencia, el no determinismo y el estado local .
La semántica denotacional se ha desarrollado para lenguajes de programación modernos que usan capacidades como concurrencia y excepciones , por ejemplo, Concurrent ML , [4] CSP , [5] y Haskell . [6] La semántica de estos lenguajes es compositiva en el sentido de que el significado de una frase depende de los significados de sus subfrases. Por ejemplo, el significado de la expresión aplicativa f(E1,E2)
se define en términos de la semántica de sus subfrases f, E1 y E2. En un lenguaje de programación moderno, E1 y E2 se pueden evaluar simultáneamente y la ejecución de uno de ellos puede afectar al otro al interactuar a través de objetos compartidos, lo que hace que sus significados se definan en términos de cada uno. Además, E1 o E2 pueden lanzar una excepción que podría terminar la ejecución del otro. Las secciones siguientes describen casos especiales de la semántica de estos lenguajes de programación modernos.
La semántica denotacional se atribuye a una frase de programa como una función de un entorno (que contiene los valores actuales de sus variables libres) a su denotación. Por ejemplo, la frase n*m
produce una denotación cuando se le proporciona un entorno que tiene un enlace para sus dos variables libres: n
y m
. Si en el entorno n
tiene el valor 3 y m
tiene el valor 5, entonces la denotación es 15. [2]
Una función se puede representar como un conjunto de pares ordenados de argumentos y valores de resultados correspondientes. Por ejemplo, el conjunto {(0,1), (4,3)} denota una función con resultado 1 para el argumento 0, resultado 3 para el argumento 4 y sin definir en caso contrario.
Consideremos por ejemplo la función factorial , que podría definirse recursivamente como:
int factorial ( int n ) { si ( n == 0 ) entonces devuelve 1 ; de lo contrario devuelve n * factorial ( n -1 ); }
Para darle un significado a esta definición recursiva, la denotación se construye como el límite de aproximaciones, donde cada aproximación limita el número de llamadas a factorial. Al principio, empezamos sin llamadas, por lo tanto, no hay nada definido. En la siguiente aproximación, podemos agregar el par ordenado (0,1), porque esto no requiere llamar a factorial nuevamente. De manera similar, podemos agregar (1,1), (2,2), etc., agregando un par en cada aproximación sucesiva porque calcular factorial(n) requiere n+1 llamadas. En el límite obtenemos una función total de a definida en todas partes en su dominio.
Formalmente, modelamos cada aproximación como una función parcial . Nuestra aproximación consiste entonces en aplicar repetidamente una función que implementa "hacer una función factorial parcial más definida", es decir , comenzando con la función vacía (conjunto vacío). F podría definirse en código de la siguiente manera (usando for ):Map<int,int>
int factorial_no_recursivo ( Map < int , int > factorial_menos_definido , int n ) { si ( n == 0 ) entonces devuelve 1 ; de lo contrario si ( fprev = lookup ( factorial_menos_definido , n -1 )) entonces devuelve n * fprev ; de lo contrario devuelve NO_DEFINIDO ; } Mapa < int , int > F ( Mapa < int , int > factorial_menos_definido ) { Mapa < int , int > nuevo_factorial = Mapa . empty (); para ( int n en todos < int > ()) { si ( f = factorial_no_recursivo ( factorial_menos_definido , n ) != NO_DEFINIDO ) nuevo_factorial . put ( n , f ); } devolver nuevo_factorial ; }
Luego podemos introducir la notación F n para indicar F aplicada n veces .
Este proceso iterativo construye una secuencia de funciones parciales de a . Las funciones parciales forman un orden parcial de cadena completa utilizando ⊆ como ordenamiento. Además, este proceso iterativo de mejores aproximaciones de la función factorial forma una aplicación expansiva (también llamada progresiva) porque cada una utiliza ⊆ como ordenamiento. Entonces, por un teorema de punto fijo (específicamente el teorema de Bourbaki-Witt ), existe un punto fijo para este proceso iterativo.
En este caso, el punto fijo es el límite superior mínimo de esta cadena, que es la factorial
función completa, que puede expresarse como la unión
El punto fijo que encontramos es el punto menos fijo de F , porque nuestra iteración comenzó con el elemento más pequeño del dominio (el conjunto vacío). Para demostrarlo necesitamos un teorema de punto fijo más complejo, como el teorema de Knaster-Tarski .
El concepto de dominios de potencia se ha desarrollado para dar una semántica denotacional a programas secuenciales no deterministas. Escribiendo P para un constructor de dominio de potencia, el dominio P ( D ) es el dominio de los cálculos no deterministas del tipo denotado por D .
Existen dificultades con la equidad y la ilimitación en los modelos teóricos del dominio del no determinismo. [7]
Muchos investigadores han argumentado que los modelos teóricos de dominio dados anteriormente no son suficientes para el caso más general de computación concurrente . Por esta razón se han introducido varios modelos nuevos . A principios de la década de 1980, la gente comenzó a usar el estilo de semántica denotacional para dar semántica para lenguajes concurrentes. Los ejemplos incluyen el trabajo de Will Clinger con el modelo de actor ; el trabajo de Glynn Winskel con estructuras de eventos y redes de Petri ; [8] y el trabajo de Francez, Hoare, Lehmann y de Roever (1979) sobre semántica de trazas para CSP. [9] Todas estas líneas de investigación siguen bajo investigación (ver por ejemplo los diversos modelos denotacionales para CSP [5] ).
Recientemente, Winskel y otros han propuesto la categoría de profunctores como una teoría de dominio para la concurrencia. [10] [11]
Los estados (como un montón) y las características imperativas simples se pueden modelar directamente en la semántica denotacional descrita anteriormente. La idea clave es considerar un comando como una función parcial en algún dominio de estados. El significado de " x:=3
" es entonces la función que lleva un estado al estado con 3
asignado a x
. El operador de secuenciación " ;
" se denota por composición de funciones. Las construcciones de punto fijo se utilizan entonces para dar una semántica a las construcciones de bucle, como " while
".
Las cosas se complican más al modelar programas con variables locales. Un enfoque es no trabajar más con dominios, sino interpretar los tipos como funtores de alguna categoría de mundos a una categoría de dominios. Los programas se denotan entonces por funciones continuas naturales entre estos funtores. [12] [13]
Muchos lenguajes de programación permiten a los usuarios definir tipos de datos recursivos . Por ejemplo, el tipo de listas de números se puede especificar mediante
lista de tipos de datos = Contras de nat * lista | Vacío
Esta sección trata únicamente de estructuras de datos funcionales que no pueden cambiar. Los lenguajes de programación imperativos convencionales normalmente permitirían que se modificaran los elementos de una lista recursiva de este tipo.
Para otro ejemplo: el tipo de denotaciones del cálculo lambda no tipificado es
tipo de datos D = D de ( D → D )
El problema de resolver ecuaciones de dominios se relaciona con encontrar dominios que modelen este tipo de tipos de datos. Un enfoque, en términos generales, consiste en considerar la colección de todos los dominios como un dominio en sí mismo y luego resolver la definición recursiva que se encuentra allí.
Los tipos de datos polimórficos son tipos de datos que se definen con un parámetro. Por ejemplo, el tipo de α list
s se define mediante
tipo de datos α lista = Contras de α * α lista | Vacío
Las listas de números naturales, entonces, son de tipo nat list
, mientras que las listas de cadenas son de tipo string list
.
Algunos investigadores han desarrollado modelos teóricos de dominio del polimorfismo. Otros investigadores también han modelado el polimorfismo paramétrico dentro de las teorías de conjuntos constructivos.
Un área de investigación reciente ha involucrado la semántica denotacional para lenguajes de programación basados en objetos y clases. [14]
Tras el desarrollo de lenguajes de programación basados en lógica lineal , se ha dado semántica denotacional a los lenguajes para uso lineal (ver por ejemplo redes de prueba , espacios de coherencia ) y también complejidad de tiempo polinomial. [15]
El problema de la abstracción total para el lenguaje de programación secuencial PCF fue, durante mucho tiempo, una gran cuestión abierta en la semántica denotacional. La dificultad con PCF es que es un lenguaje muy secuencial. Por ejemplo, no hay forma de definir la función paralela-o en PCF. Es por esta razón que el enfoque que utiliza dominios, como se presentó anteriormente, produce una semántica denotacional que no es completamente abstracta.
Esta cuestión abierta se resolvió en gran parte en la década de 1990 con el desarrollo de la semántica de juegos y también con técnicas que involucraban relaciones lógicas . [16] Para más detalles, consulte la página sobre PCF.
A menudo resulta útil traducir un lenguaje de programación a otro. Por ejemplo, un lenguaje de programación concurrente podría traducirse a un cálculo de procesos ; un lenguaje de programación de alto nivel podría traducirse a un código de bytes. (De hecho, la semántica denotacional convencional puede considerarse como la interpretación de los lenguajes de programación en el lenguaje interno de la categoría de dominios).
En este contexto, las nociones de la semántica denotacional, como la abstracción completa, ayudan a satisfacer las preocupaciones de seguridad. [17] [18]
A menudo se considera importante conectar la semántica denotacional con la semántica operacional . Esto es especialmente importante cuando la semántica denotacional es más bien matemática y abstracta, y la semántica operacional es más concreta o más cercana a las intuiciones computacionales. Las siguientes propiedades de una semántica denotacional suelen ser de interés.
En el caso de la semántica tradicional, la adecuación y la abstracción completa pueden entenderse, en líneas generales, como el requisito de que "la equivalencia operacional coincida con la igualdad denotacional". En el caso de la semántica denotacional en modelos más intensionales, como el modelo de actores y los cálculos de procesos , existen diferentes nociones de equivalencia dentro de cada modelo, por lo que los conceptos de adecuación y de abstracción completa son un tema de debate y más difíciles de precisar. Además, la estructura matemática de la semántica operacional y la semántica denotacional pueden llegar a ser muy parecidas.
Otras propiedades deseables que podríamos desear mantener entre la semántica operacional y denotacional son:
Un aspecto importante de la semántica denotacional de los lenguajes de programación es la composicionalidad, por la cual la denotación de un programa se construye a partir de las denotaciones de sus partes. Por ejemplo, considere la expresión "7 + 4". La composicionalidad en este caso consiste en proporcionar un significado para "7 + 4" en términos de los significados de "7", "4" y "+".
Una semántica denotacional básica en la teoría de dominios es compositiva porque se da de la siguiente manera. Comenzamos considerando fragmentos de programas, es decir, programas con variables libres. Un contexto de tipificación asigna un tipo a cada variable libre. Por ejemplo, en la expresión ( x + y ) se podría considerar en un contexto de tipificación ( x : nat
, y : nat
). Ahora damos una semántica denotacional a fragmentos de programas, utilizando el siguiente esquema.
nat
debería ser el dominio de los números naturales: 〚nat
〛= ⊥ .nat
, y : nat
〛= ⊥ × ⊥ . Como caso especial, el significado del contexto de tipificación vacío, sin variables, es el dominio con un elemento, denotado 1.nat
〛:1→ ⊥ es la función "7" constante, mientras que 〚x : , y : ⊢ x + y : 〛: ⊥ × ⊥ → ⊥ es la función que suma dos números.nat
nat
nat
Ahora, el significado de la expresión compuesta (7+4) se determina componiendo las tres funciones 〚⊢7: nat
〛:1→ ⊥ , 〚⊢4: 〛:1→ ⊥ , y 〚x : , y : ⊢ x + y : 〛: ⊥ × ⊥ → ⊥ .nat
nat
nat
nat
De hecho, este es un esquema general para la semántica denotacional compositiva. No hay nada específico sobre dominios y funciones continuas aquí. Uno puede trabajar con una categoría diferente en su lugar. Por ejemplo, en la semántica de juegos, la categoría de juegos tiene juegos como objetos y estrategias como morfismos: podemos interpretar tipos como juegos y programas como estrategias. Para un lenguaje simple sin recursión general, podemos arreglárnoslas con la categoría de conjuntos y funciones . Para un lenguaje con efectos secundarios, podemos trabajar en la categoría de Kleisli para una mónada. Para un lenguaje con estado, podemos trabajar en una categoría de functor . Milner ha abogado por modelar la ubicación y la interacción trabajando en una categoría con interfaces como objetos y bigrafos como morfismos. [20]
Según Dana Scott (1980): [21]
Según Clinger (1981): [22] : 79
Algunos trabajos en semántica denotacional han interpretado los tipos como dominios en el sentido de la teoría de dominios, que puede considerarse una rama de la teoría de modelos , lo que lleva a conexiones con la teoría de tipos y la teoría de categorías . En el ámbito de la informática, existen conexiones con la interpretación abstracta , la verificación de programas y la comprobación de modelos .
{{cite book}}
: CS1 maint: location missing publisher (link){{cite book}}
: |work=
ignorado ( ayuda )