ASIGNACIO N Y ASIGNACIO COMPROBACIO COMPROBA CIO N DINA MICA DE TIPOS DINA El análisis de tipos tiene por objetivo asegurar que el uso de los objetos definidos es correcto: esto es, que su uso se atiene a la semántica de su definición
[Escriba el subtítulo del documento]
Contenido Introducción ........................................................................................................................................ 2 Análisis de Tipos: Conceptos Básicos .................................................................................................. 3 Expresiones de Tipo, Sistemas de Tipos y Comprobadores de Tipos.............................................. 3 Definición 6.1.1 ........................................................................................................................... 3 Definición 6.1.2 ........................................................................................................................... 3 Comprobación de tipos ................................................................................................................... 3 Definición 6.1.3 ........................................................................................................................... 3 Tipado Estático y Tipado Dinámico ................................................................................................. 5 Definición 6.1.4 ........................................................................................................................... 5 Definición 6.1.5 ........................................................................................................................... 6 Tipado Fuerte y Tipado Débil .......................................................................................................... 7 Definición 6.1.6 ........................................................................................................................... 7 Tipado Fuerte (strong typing) ......................................................................................................... 7 Sobrecarga, Polimorfismo e Inferencia de Tipos ............................................................................ 8 Definición 6.1.7 ........................................................................................................................... 8 Definición 6.1.8 ........................................................................................................................... 9 Definición 6.1.9 ........................................................................................................................... 9 Definición 6.1.10 ......................................................................................................................... 9 Equivalencia de Expresiones de Tipo ............................................................................................ 10 Definición 6.1.11 ....................................................................................................................... 10 Compatibilidad de tipos ................................................................................................................ 11 Métodos: ................................................................................................................................... 11 Duck Typing ................................................................................................................................... 13 Definición 6.1.12 ....................................................................................................................... 13 Subtipos y tipos derivados ............................................................................................................ 15 Subtipo ...................................................................................................................................... 15 Bibliografía ........................................................................................................................................ 16
Introducción Los lenguajes de programación con comprobación estática de tipos poseen beneficios incuestionables
•
Detección temprana de errores
•
Mayores oportunidades para realizar o ptimizaciones
•
Entornos de desarrollo
•
Documentación …
No obstante, los lenguajes con comprobación dinámica de tipos son utilizados comúnmente en diversos escenarios del desarrollo del software (p. ej. ingeniería web o el desarrollo rápido de prototipos) dado que ofrecen
•
Simplicidad
•
Alto nivel de flexibilidad
•
Metaprogramación
•
Generación dinámica de código …
Dado que ambos enfoques ofrecen diferentes beneficios, han surgido lenguajes de programación con comprobación híbrida (estática y dinámica) de tipos o Algunos lenguajes con comprobación estática de tipos también han incorporado la posibilidad de incluir tipos dinámicos en su sistema de tipos
TIOBE Programming Community Index (Agosto 2013)
Análisis de Tipos: Conceptos Básicos En la mayoría de los lenguajes los objetos manipulados son declarados en alguna parte del programa y usados en otras. Ya dijimos que el an álisis de ámbito es el cálculo de la función que asigna a un uso de un objeto la definición que se le aplica. El análisis de tipos tiene por objetivo asegurar que el uso de los objetos definidos es correcto: esto es, que su uso se atiene a la semántica de su definición; por ejemplo, que un array de enteros no es llamado como función o que no se intenta incrementar una función o que el valor retornado por una función es de la naturaleza descrita en su definición.
Expresiones de Tipo, Sistemas de Tipos y Comprobadores de Tipos Definición 6.1.1 Una forma adecuada de representar los tipos dentro de un compilador es
usando un lenguaje de expresiones de tipo. Un lenguaje de las expresiones de tipo debe describir de manera clara y sencilla los tipos del lenguaje fuente. No confunda este lenguaje con el sub-lenguaje del lenguaje fuente que consiste en las declaraciones o definiciones. No tienen por que ser iguales. El compilador traduce las declaraciones de tipo en expresiones de tipo. El lenguaje de las expresiones de tipo es la representación interna que el compilador tiene de estas declaraciones y depende del compilador. El lenguaje de las declaraciones no. Definición 6.1.2 Un sistema de tipos de un lenguaje/compilador es el conjunto de reglas
del lenguaje (que es traducido e interpretado por el compilador que permiten asignar expresiones de tipo a las instancias de uso de los objetos del programa. Si bien el sistema de tipos es una propiedad del lenguaje, no es raro que los compiladores introduzcan modificaciones en el sistema de tipos del lenguaje. Por ejemplo en Pascal el tipo de un array incluye los índices del array6.1). Esto y las reglas de equivalencia de tipos de Pascal limitan gravemente la genericidad de las funciones en Pascal. Por eso algunos compiladores Pascal permiten en una llamada a función la compatibilidad de tipos entre arrays de diferente tamaño y diferentes conjuntos de índices. Desgraciadamente la forma en la que lo hacen puede diferir de compilador a compilador.
Comprobación de tipos Definición 6.1.3 Un comprobador de tipos verifica que el uso de los objetos en los
constructos de uso se atiene a lo especificado en sus declaraciones o definiciones de acuerdo a las reglas especificadas por el sistema de tipos. Generalización de los conceptos de operandos y operadores para incluir a subprogramas y sentencia de asignación: • •
Subprogramas → operadores cuyos operandos son sus parámetros Asignación → operador binario cuyos operandos son la variable a la que se asigna el valor y la expresión a evaluar
Comprobación de tipos: actividad que nos asegura que los operandos de un operador son de tipos compatibles Tipos compatibles: un tipo legal para el operador o un tipo que atendiendo a determinadas reglas del lenguaje puede ser convertido implícitamente mediante código generado por el compilador en un tipo legal. Esta conversión automática se denomina coacción (coercion) Error de tipos: aplicación de un operador a un operando de tipo inapropiado
Si todas las ligaduras de tipos a variables son estáticas pueden realizarse de forma estática
⇒
las comprobaciones de tipo
Si hay ligadura dinámica de tipos a variables ⇒ debe realizarse una comprobación dinámica de tipos (más costosa que en tiempo de compilación) LP (Pascal, Ada, C) que permiten el almacenamiento en la misma celda de memoria de valores de tipos diferentes en momentos distintos de la ejecución (registros variantes y uniones) ⇒ la comprobación de tipos para estas clases de datos debe ser dinámica ⇒ requiere mantener durante la ejecución el tipo actual asociado a la celda de memoria ⇒ no todos los errores pueden detectarse con una comprobación estática de tipos
Tipado Estático y Tipado Dinámico Definición 6.1.4 Un lenguaje de programación tiene tipado estático si su comprobación de
tipos ocurre en tiempo de compilación sin tener que comprobar equivalencias en tiempo de ejecución. La comprobación estática de tipos trata de asegurar que los programas no produzcan errores durante su ejecución Sin embargo, algunos programas no pueden ser compilados a pesar de tener un comportamiento correcto durante la ejecución. Estos lenguajes siguen un enfoque pesimista
public class Test { public static void Main() { object[] v = new object[10]; int suma = 0; for (int i = 0; i < 10; i++) { v[i] = i+1; suma += v[i]; // Error C. } } }
// Error C.
public class Test { public static void Main() { object o; Console.Write(o.ToString()); // Error de ejecución } } }
// Error de ejecución
Un lenguaje de programación tiene tipado dinámico si el lenguaje realiza comprobaciones de tipo en tiempo de ejecución. En un sistema de tipos dinámico los tipos suelen estár asociados con los valores no con las variables. Definición 6.1.5 El tipado dinámico hace más sencilla la escritura de metaprogramas:
programas que reciben como datos otros códigos y los manipulan para producir nuevos códigos. Parse::Eyapp y Parse::Treeregexp son ejemplos de metaprogramación. Cada vez que escribimos un procesador de patrones, templates o esqueletos de programación estamos haciendo meta-programación. La comprobación dinámica de tipos realiza todas las comprobaciones de tipo durante la ejecución. Estos lenguajes son demasiado optimistas, dado que compilan programas que podrían ser detectados como erróneos durante la compilación
public class Test { public static void Main() { dynamic myObject = "StaDyn"; System.Console.Write(myObject*2); // Sin errores de compilación } }
// Sin errores
El lenguaje en el que se escribe el metaprograma se denomina metalenguaje. El lenguaje al que se traduce el metaprograma se denomina lenguaje objeto. La capacidad de un lenguaje de programación para ser su propio metalenguaje se denomina reflexividad. Para que haya reflexión es conveniente que el código sea un tipo de estructura de datos soportado por el lenguaje al mismo nivel que otros tipos básicos y que sea posible traducir dinámicamente texto a código.
Tipado Fuerte y Tipado Débil Definición 6.1.6 Aunque el significado de los términos Fuertemente Tipado y su contrario
Débilmente Tipado varían con los autores, parece haber consenso en que los lenguajes con tipado fuerte suelen reunir alguna de estas características: •
• • • •
La comprobación en tiempo de compilación de las violaciones de las restricciones impuestas por el sistema de tipos. El compilador asegura que para cualesquiera operaciones los operandos tienen los tipos válidos. Toda operación sobre tipos inválidos es rechazada bien en tiempo de compilación o de ejecución. Algunos autores consideran que el término implica desactivar cualquier conversión de tipos implícita. Si el programador quiere una conversión deberá explicitarla. La ausencia de modos de evadir al sistema de tipos. Que el tipo de un objeto de datos no varíe durante la vida del objeto. Por ejemplo, una instancia de una clase no puede ver su clase alterada durante la ejecución.
Tipado Fuerte (strong typing) Un lenguaje tiene Tipado Fuerte si los errores de tipos se detectan siempre ⇒ es necesario determinar los tipos de todos los operandos, ya sea en tiempo de compilación o de ejecución Pascal
•
Cercano a tener Tipado Fuerte pero no realiza comprobación de tipos en los registros variantes (incluso puede omitirse la etiqueta discriminatoria en dichos registros)
Ada
• •
Resuelve el problema de los registros variantes realizando comprobación dinámica de tipos (sólo en este caso) Tiene una función de biblioteca que permite extraer un valor de una variable de cualquier tipo (como una cadena de bits) y usarlo como un tipo diferente (no es una conversión de tipos) ⇒ se trata de una suspensión temporal de la comprobación de tipos
C
• No tiene Tipado Fuerte por: − No se realiza comprobación de tipos sobre las uniones − Permite funciones con parámetros sobre los que no se realiza comprobación de tipos Java
•
Tiene Tipado Fuerte (no hay uniones)
ML y Haskell
• •
Poseen Tipado Fuerte Los tipos de los parámetros de las funciones (y de estas mismas) se conocen en tiempo de compilación (ya sea por declaración del usuario o por inferencia de tipos)
Sobrecarga, Polimorfismo e Inferencia de Tipos Un símbolo se dice sobrecargado si su significado varía dependiendo del contexto. En la mayoría de los lenguajes los operadores aritméticos suelen estar sobrecargados, dado que se sustancian en diferentes algoritmos según sus operando sean enteros, flotantes, etc. En algunos lenguajes se permite la sobrecarga de funciones. así es posible tener dos funciones llamadas min:
A la hora de evaluar el tipo de las expresiones es el contexto de la llamada el que determina el tipo de la expresión: float x,y; int a,b; string c,d; u = min(x,y); /* Puede que correcto: x e y seran truncados a enteros. Tipo entero */ v = min(a,b); /* Correcto: Tipo devuelto es entero */ w = min(c,d); /* Correcto: Tipo devuelto es string */ t = min(x,c); /* Error */ Definición 6.1.7 La inferencia de tipos hace referencia a aquellos algoritmos que deducen
automáticamente en tiempo de compilación - sin información adicional del programador, o bien con anotaciones parciales del programador - el tipo asociado con un uso de un objeto del programa. Un buen número de lenguajes de programación funcional permiten implantar inferencia de tipos (Haskell, OCaml, ML, etc). Véase como ejemplo de inferencia de tipos la siguiente sesión en OCaml: pl@nereida:~/src/perl/attributegrammar/LanguageAttributeGrammar-.08/examples$ ocaml Objective Caml version 3.09.2 # let minimo = fun i j -> if i 'a -> 'a = # minimo 2 3;; - : int = 2 # minimo 4.9 5.3;; - : float = 4.9 # minimo "hola" "mundo";; - : string = "hola"
El compilador OCaml infiere el tipo de las expresiones. Así el tipo asociado con la función minimo es 'a -> 'a -> 'a que es una expresión de tipo que contiene variables de tipo. El
operador -> es asociativo a derechas y asi la expresión debe ser leída como 'a -> ('a -> 'a). Básicamente dice: es una función que toma un argumento de tipo 'a (donde 'a es una variable tipo que será instanciada en el momento del uso de la función) y devuelve una función que toma elementos de tipo 'a y retorna elementos de tipo 'a. Definición 6.1.8 Aunque podría pensarse que una descripción más adecuada del tipo de la
función minimo fuera 'a x 'a -> 'a , lo cierto es que en algunos lenguajes funcionales es usual que todas las funciones sean consideradas como funciones de una sóla variable. La función de dos variables 'a x 'a -> 'a puede verse como una función 'a -> ('a -> 'a). En efecto la función minimo cuando recibe un argumento retorna una función: # let min_mundo = minimo "mundo";; val min_mundo : string -> string = # min_mundo "pedro";; - : string = "mundo" # min_mundo "antonio";; - : string = "antonio" # min_mundo 4;; This expression has type int but is here used with type string # min_mundo(string_of_int(4));; - : string = "4"
Esta estrategia de reducir funciones de varias variables a funciones de una variable que retornan funciones de una variable se conoce con el nombre de currying o aplicación parcial . Definición 6.1.9 El polimorfismo es una propiedad de ciertos lenguajes que permite una
interfaz uniforme a diferentes tipos de datos. Se conoce como función polimorfa a una función que puede ser aplicada o evaluada sobre diferentes tipos de datos. Un tipo de datos se dice polimorfo si es un tipo de datos generalizado o no completamente especificado. Por ejemplo, una lista cuyos elementos son de cualquier tipo. Definición 6.1.10 Se llama Polimorfismo Ad-hoc a aquel en el que el número de
combinaciones que pueden usarse es finito y las combinaciones deben ser definidas antes de su uso. Se habla de polimorfismo paramétrico si es posible escribir el código sin mención específica de los tipos, de manera que el código puede ser usado con un número arbitrario de tipos. Por ejemplo, la herencia y la sobrecarga de funciones y métodos son mecanismos que proveen polimorfismo adhoc. Los lenguajes funcionales, como OCaml suelen proveer polimorfismo paramétrico. En OOP el polimorfismo paramétrico suele denominarse programación genérica.
Equivalencia de Expresiones de Tipo La introducción de nombres para las expresiones de tipo introduce una ambiguedad en la interpretación de la equivalencia de tipos. Por ejemplo, dado el código:
typedef int v10[10]; v10 a; int b[10]; ¿Se considera que a y b tienen tipos compatibles? Definición 6.1.11 Se habla de equivalencia de tipos estructural cuando los nombres de tipo
son sustituidos por sus definiciones y la equivalencia de las expresiones de tipo se traduce en la equivalencia de sus árboles sintácticos o DAGs. Si los nombres no son sustituidos se habla de equivalencia por nombres o de equivalencia de tipos nominal . Si utilizamos la opción de sustituir los nombres por sus definiciones y permitimos en la definición de tipo el uso de nombres de tipo no declarados se pueden producir ciclos en el grafo de tipos. El lenguaje C impide la presencia de ciclos en el grafo de tipos usando dos reglas: 1. Todos los identificadores de tipo han de estar definidos antes de su uso, con la excepción de los punteros a registros no declarados 2. Se usa equivalencia estructural para todos los tipos con la excepción de las struct para las cuales se usa equivalencia nominal Por ejemplo, el siguiente programa: nereida:~/src/perl/testing> cat -n typeequiv.c #include typedef struct { int x, y; struct record *next; } record; record z,w; struct recordcopy { int x, y; struct recordcopy *next; } r,k; main() { k = r; /* no produce error */ z = w; /* no produce error */ r = z; } Produce el siguiente mensaje de error: nereida:~/src/perl/testing> gcc -fsyntax-only typeequiv.c typeequiv.c: En la función 'main': typeequiv.c:19: error: tipos incompatibles en la asignación
Compatibilidad de tipos Métodos: Compatibilidad de tipos nominal : dos variables tienen tipos compatibles si están ligadas al
mismo nombre de tipo Ej: type T = array [1..10] of integer; var a, b: T; begin a := b; Fácil de implementar pero muy restrictiva. Ej. Usando un lenguaje con interpretación estricta de la compatibilidad de tipos nominal: type tipo_indice = 1..100; var contador: integer; indice: tipo_indice; begin contador := indice; indice := contador; Compatibilidad de tipos estructural : dos variables tienen tipos compatibles si sus tipos
tienen la misma estructura Ej: type T = array [1..10] of integer; S = array [1..10] of integer; var a: S; b: T; begin a := b; Debe compararse la estructura de ambos tipos, pero: − ¿Son compatibles dos registros con la misma estructura pero con nombres de campos diferentes? − ¿Son compatibles arrays del mismo tipo y con el mismo número de elementos pero con rangos de índices diferentes (0..9 / 1..10)? − ¿Son compatibles tipos enumerados con el mismo número de componentes pero literales diferentes?
No acepta diferencias entre tipos con la misma estructura Ej: type longitud = real; tiempo = real; var l: longitud; t: tiempo; → l y t son compatibles (tienen la misma estructura), aunque quizás no deberían considerarse equivalentes: son abstracciones de diferentes categorías de valores del problema. Equivalencia de declaración : dos variables tienen tipos compatibles si uno de los tipos está
definido con el nombre del otro Ej: type T = array [1..10] of integer; S = T; var a: S; b: T; begin a := b; La mayoría de LP utilizan combinaciones de estos tres métodos de compatibilidad Tipo anónimo: tipo asociado directamente con una variable mediante una declaración sin
proporcionarle un nombre Ej. en Ada: a, b: array (1..10) of INTEGER; c: array (1..10) of INTEGER; → La interpretación es obvia usando compatibilidad de tipos estructural → ¿Compatibilidad nominal? Ej. Ada a, b y c son incompatibles
Duck Typing En lenguajes dinámicos una forma habitual de equivalencia de tipos es el tipado pato: Definición 6.1.12 Se denomina duck typing o tipado pato a una forma de tipado dinámico
en la que el conjunto de métodos y propiedades del objeto determinan la validez de su uso. Esto es: dos objetos pertenecen al mismo tipo-pato si implementan/soportan la misma interfaz independientemente de si tienen o no una relación en la jerarquía de herencia. El nombre del concepto se refiere a la prueba del pato, una humorada de razonamiento inductivo atribuida a James Whitcomb Riley (ver Historia más abajo), que pudo ser como sigue: If it waddles like a duck, and quacks like a duck, it's a duck!. "Cuando veo un ave que camina como un pato, nada como un pato y suena como un pato, a esa ave yo la llamo un pato." En los lenguajes de programación orientados a objetos, se conoce como duck typing el estilo de tipificación dinámica de datos en que el conjunto actual de métodos y propiedades determina la validez semántica, en vez de que lo hagan la herencia de una clase en particular o la implementación de una interfaz específica. En duck typing, el programador solo se ocupa de los aspectos del objeto que van a usarse, y no del tipo de objeto que se trata. Por ejemplo en un lenguaje sin duck-typing uno puede crear una función que toma un objeto de tipo Pato y llama los métodos "caminar" y "parpar" de ese objeto. En un lenguaje con duck-typing, la función equivalente tomaría un objeto de cualquier tipo e invocaría los métodos caminar y parpar. Si el objeto tratado no tiene los métodos pedidos, la función enviará una señal de error en tiempo de ejecución. Este hecho de que la función acepte cualquier tipo de objeto que implemente correctamente los métodos solicitados es lo que evoca la cita precedente y da nombre a la forma de tipificación. El Duck typing usualmente es acompañado por el hábito de no probar el tipo de los argumentos en los métodos y funciones, y en vez de eso confiar en la buena documentación, el código claro y la prueba para asegurar el uso correcto. Los usuarios de lenguajes con tipificado estático al iniciarse con lenguajes de tipificado dinámico a menudo se ven tentados a agregar chequeos de tipo estáticos (previos a ejecución), desaprovechando la flexibilidad y beneficios del duck typing y restringiendo el dinamismo del lenguaje. Considera el siguiente pseudocódigo para un lenguaje con duck-typing: función calcular(a, ejemplo1 = calcular ejemplo2 = calcular ejemplo3 = calcular
b, c) => devuelve (a+b)*c (1, 2, 3) ([1, 2, 3], [4, 5, 6], 2) ('manzanas ', 'y naranjas, ', 3)
mostrar a_cadena ejemplo1 mostrar a_cadena ejemplo2 mostrar a_cadena ejemplo3 En el ejemplo, cada vez que se llama la función calcular se pueden emplear objetos sin relación de herencia (números, listas y cadenas). En tanto y en cuanto los objetos soporten los métodos "+" y "*" la operación tendrá éxito. Si se traduce este algoritmo a Ruby o Python por ejemplo, el resultado de la ejecución será: 9 [1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6] manzanas y naranjas, manzanas y naranjas, manzanas y naranjas,
Así, el duck typing permite polimorfismo sin herencia. El único requerimiento de la función calcular para las variables es que soporten los métodos "+" y "*".
Subtipos y tipos derivados Subtipo: versión (posiblemente restringida en el rango) de un tipo existente con el que es
compatible. Evita uno de los problemas de la compatibilidad de tipos nominal • Ej. Ada: subtype Natural is Integer range 0.. Integer’Last; subtype Temperatura is Integer range -20..45; n: Natural; i: Integer; t: Temperatura; → Las siguientes asignaciones son posibles: n := i; i := n; t := i; → Se necesita una comprobación en tiempo de ejecución que asegure que el valor que se asigna está en el rango adecuado → Para la función: function "**" (i: Integer; n: Natural) return Integer; Y la llamada j ** k; El compilador comprueba que j y k sean del tipo Integer (o de uno de sus subtipos). Durante la ejecución se asegura que el valor actual de k esté en el rango de Natural. Un lenguaje con compatibilidad de tipos nominal sólo aceptaría que j fuese del tipo Integer y n del tipo Natural. Tipo derivado: nuevo tipo basado en algún otro previamente definido con el que es incompatible, aunque sean estructuralmente idénticos. Heredan todas las propiedades de su tipo padre. (También pueden incluir restricciones sobre el rango) Ej. Ada: type Longitud is new Float; type Tiempo is new Float; → Las variables de estos tipos son incompatibles entre ellas y con otras de tipo float (esta regla no se aplica a los literales como 12.5)
Bibliografía http://nereida.deioc.ull.es/~pl/perlexamples/node178.html http://icsdonald.files.wordpress.com/2012/02/ligadura-de-una-variable.pdf http://es.wikipedia.org/wiki/Duck_typing http://babel.ls.fi.upm.es/prole2013/miguel_garcia_rodriguez.pdf