Lenguajes fuertemente, débilmente, estáticamente y dinámicamente tipados

Shoot your foot

Uno de los aspectos más importantes de cualquier programa es, sin duda, el tema de los datos.

Los datos son el soporte de la información, y la información es el corazón de cualquier programa.

Los lenguajes de programación utilizan las variables como contenedores de datos, para manipularlos y almacenarlos en una amplia variedad de maneras. Está claro que los datos tienen distintos tipos. A nadie se le escapa que no es lo mismo un nombre de persona que su edad... el primero es un texto y el segundo es un número...

Los distintos lenguajes de programación tienen en cuenta esta particularidad de los datos, y por lo tanto, todos tienen alguna forma de distinción entre los tipos de datos.

No obstante, cada uno tiene una forma de afrontar esta particularidad. Ello ha llevado a podamos en cierto modo "clasificar" los lenguajes de programación por la forma en la que tratan con el problema de los tipos entre

  • Dinámica y estáticamente tipados
  • Fuerte y débilmente tipados

El tema es, sin duda, controvertido. Cada sesudo investigador o aficionado al tema podrá darle mil definiciones y argumentos acerca de lo que significa cada uno de esos términos... pero en nuestra humilde opinión, no conviene obsesionarse ni llevar el tema al extremo.

Tener una ligera idea para entender que existen varias formas de afrontar el problema de los tipos es una cosa, y emprender una cruzada para defender no-sé-qué-cosa de un lenguaje u otro, o tratar de ponerle puertas al campo es otra.

En este artículo, matizaremos los aspectos más importantes que nos permiten distinguir la forma en que un lenguaje aborda el tema de los tipos de los datos, sin ánimo de controversia ni de sentar cátedra... y por supuesto... influenciados por nuestra propia opinión de aficionados ;-) Lo mejor que puedes hacer con un artículo como éste es leerlo, no creértelo del todo, contrastarlo con otras fuentes... y formarte tu propia opinión.

La controversia entre éstos tipados tiene fuertes raíces en el hecho de que cuando se diseña un lenguaje, se tiene en mente si va a ser compilado o interpretado. Las diferencias en el diseño de un compilador o de un intérprete son radicales. Además, debemos fijarnos en el hecho de que un mismo lenguaje, generalmente se compila o generalmente se interpreta. Por ejemplo, el lenguaje C, o C++ o C#, o Java... generalmente es compilado. El lenguaje JavaScript, o PHP o Python, generalmente se interpretan.

El tratamiento de tipos en un intérprete puede, en general, disfrutar de una riqueza mucho mayor en las estructuras que internamente se utilizan para representar a las variables. El compilador, por su parte, depende de una traducción, habitualmente a código máquina, bien de una máquina real o bien de una máquina virtual, como en el caso de .net o de Java. Incluso hay compiladores que traducen de un de un lenguaje de alto nivel a otro... pero en cualquier caso, en un compilador se realiza una traducción de todo un programa completo de golpe a un programa equivalente en otro lenguaje. Eso hace que las estructuras para representar variables tengan una flexibilidad algo menor, y, si se quiere dotar de mecanismos más flexibles o más potentes a un compilador, generalmente redunde en un mayor consumo de tiempo y de memoria... y también generalmente, a los compiladores se les suele exigir rapidez en el código que generan y consumo de recursos reducido.

Estos factores, en cierto modo, condicionan cómo los lenguajes tratan los tipos.

Tipos estáticos, tipos dinámicos.

(static typing, dinamic typing)

En un lenguaje estáticamente tipado, cada variable debe ser declarada con un tipo. Eso ocurre por ejemplo en C# o Java. Para utilizar una variable de tipo entero, es necesario indicar que en efecto, es de ese tipo. El tipado estático es típico de los lenguajes compilados. El tipo no se altera hasta que la variable se sale de ámbito y se extingue. Éste comportamiento se aplica tanto a variables locales (a un método o función), a parámetros (de un método o función), a varibles de instancia (de un objeto), a variables de clase ("static"), o a variables globales (en lenguajes no orientados a objetos).

Por ejemplo, en C# o java, para utilizar una variable de nombre "i" que contenga un entero

//en C# o java
int i; //Primero se declara
i=3; //y luego se utiliza

No es posible asignar un valor de otro tipo a una variable de un tipo concreto:

//en C# o java
int i; 
i=3; //correcto
i="hola"; //incorrecto

En un lenguaje dinámicamente tipado, no suele ser necesario declarar el tipo de las variables. Es típico de los intérpretes. Ocurre en lenguajes como PHP o Python. Las variables empiezan a existir cuando se les da valor. Los lenguajes dinámicamente tipados tienen en cuenta los tipos, desde luego... pero no de las variables, sino de su contenido.

Una misma variable puede contener en un instante dado un dato de un determinado tipo, y en otro instante, puede contener otro dato de otro tipo.

Por ejemplo, en Python, podemos utilizar una variable "i" para contener un entero, sin necesidad de indicar que va a contener un entero. (La almohadilla # indica un comentario)

#En python,la variable comienza 
i=3 #a existir al darle un valor
i="hola" #luego, puede contener un valor de otro tipo

Vamos a contarlo de otro modo. Imagina que las variables fueran cajas de cartón (bueno, al fin y al cabo, una caja de cartón sirve para contener cosas):

  • En los lenguajes estáticamente tipados se le asigna el tipo a la caja. Es como si en la caja, pusieramos una etiqueta: "esta caja sólo puede contener zapatos". A partir de ese momento, en esa caja sólo se pueden meter zapatos, hasta que la caja sea destruida.
  • En los lenguajes dinámicamente tipados, es el contenido el que tiene el tipo. En la caja no colocamos ninguna indicación acerca del contenido. Si metemos un par de zapatos será una caja de zapatos, y si metemos un juguete será una caja de juguetes. Nadie impide que metamos cualquier cosa. Bueno... la segunda opción parece, al menos intuitivamente, más sencilla... más flexible... ¿Por qué no se utiliza siempre? Ah... amigo... muy sencillo: en la segunda opción, hay que abrir las cajas para saber lo que contienen.

Con el tipado estático, dadas dos variables enteras, si después queremos hacer una suma, seguro que se puede hacer... sin mirar, dado que el compilador sabe que seguro que contienen un entero, por lo tanto, la suma se puede hacer sin comprobaciones previas.

//C# o Java
int i1=3;
int i2=5;
int i3=i1+i2; //No se hacen comprobaciones en la suma:
  // seguro que i1 e i2 contienen un entero. 

Con el tipado dinámico, las operaciones son algo más costosas... el intérprete deberá obtener el contenido de las variables ("abrir las cajas") y decidir qué operación hacer y cómo, suponiendo que ésta sea posible.

En Python, la tercera línea necesita comprobaciones adicionales a nivel interno, con respecto al tipado estático.

#python
i1=3
i2=5
i3=i1+i2

Fuertemente tipados, débilmente tipados

(strong typing, weak typing)

En este punto, la controversia es algo mayor. No hay un criterio unánime acerca del grado de fortaleza o debilidad en los tipos.

Comnunmente, se entiende que un lenguaje tiene un sistema de tipos más fuerte si para una determinada operación, que requiera un determinado tipo de datos, el lenguaje no acepta tipos que pudieran ser "compatibles".

Por ejemplo, si un lenguaje nos permite declarar un método o función que acepte como parámetro un número real de doble precisión (es decir, un double), consideraríamios que el lenguaje está muy fuertemente tipado si sólo permitiese llamadas o invocaciones a esa función o método pasándole como parámetro un dato con un tipo double.

A partir de ahí, podríamor ir rebajando el listón. Por ejemplo, en la situación descrita, casi todos los lenguajes permiten una serie de conversiones implícitas. Es decir, muchos lenguajes (como C# o Java, que son estáticamente tipados) permitirían invocar un método que aceptase un double como parámetro pasándole un float (un número con decimales de simple precisión), o incluso un entero. Es decir, en ese sentido, C# o Java están fuertemente tipados, pero no todo lo fuertemente tipado que se puede estar.

Todavía se podría rebajar más el listón: si en el mismo caso, el método que acepta un double, intentásemos una llamada pasándole una cadena (que podría contener en sus carácteres la representación de un double, y por lo tanto, la operación podría tener cierto sentido, ej: "3.1416"), un lenguaje débilmente tipado permitiría la llamada, e intentaría, en el interior del método o función, interpretar la cadena como un double.

Prácticamente todos los lenguajes permiten una cierta conversión implícita de tipos, especialmente si se pueden considerar seguras. Por ejemplo, convertir un entero a un double suele ser una operación segura, dado que un entero no tiene decimales, y un double sí... es decir, es un tipo más amplio. Una conversión de éste tipo puede ser implícita con un alto grado de seguridad. Al reves no es así. Si yo tengo un double (Ej: 2.68) y lo utilizo en un contexto en el que se requiere un entero, ¿qué debería hacer el lenguaje con los decimales? ¿Los quita? ¿Los redondea?

Un lenguaje débilmente tipado, en general, permite éste tipo de conversiones implícitas inseguras. Es su filosofía... que el programador se encargue de programar, y el lenguaje se encargará de hacer todo lo posible para que el programa ejecute, liberando al programador de llevar también la cuenta de posibles conversiones de tipos.

Por ejemplo, javascript es un lenguaje con un sistema de tipos mucho más débil que C# o Java. En una función de javascript ni siquiera se declaran los tipos de los parámetros o las variables, ya que el lenguaje es dinámicamente tipado.

En el ejemplo que sigue, la función mitad(a) calcula y devuelve la mitrad del parámetro a, con lo que se debería esperar que a fuera un número. Sin embargo, si se pasa una cadena, javascript intenta contemplar esa cadena como un número de manera automática.

//javascript
function mitad(a)
{
    return a/2;
}

var s="12";
document.write(mitad(s));
//se pasa una cadena a la función, sin embargo,
//javascript intenta considerar 2 como número y no
//como cadena, e imprime 6 en el documento

s="hola";
document.write(mitad(s)); 
// ahora con la misma línea, el contenido de s
// no puede ser interpretado como un número
// y se imprime el valor especial NaN (Not a Number)

Esta última forma de actuar proporciona, sin duda, una manera más sencilla de programar. Es decir, en un lenguaje débilmente tipado, el lenguaje prestará poca atención a una definición estricta de los tipos. Eso hace que el programador pueda centrarse en el objetivo del problema que resuelve, que el lenguaje intentará encargarse de todo lo referido a los tipos... sin embargo, la consecuencia lógica es que los posibles errores se detectarán más tarde, en tiempo de ejecución.

Éste hecho de permitir una mayor o menor relajación en el uso de los tipos no está tan íntimamente relacionado con la compilación o la interpretación. Ni siquiera tiene una relación evidente con el tipado estático o dinámico. Sin embargo, sí puede verse una cierta tendencia a que los lenguajes estáticamente tipados (como C# o Java) también tengan una cierta fortaleza en el tratamiento de los tipos.

La debilidad en los tipos no está tampoco íntimamente relacionada con el tipado dinámico. Python, por ejemplo, tiene un tipado más fuerte que Javascript, y en la misma situación que el en ejemplo anterior produciría un error de tipos.

#python
def mitad(a):
  return a/2

s=12
print mitad(s) # produce 6.0


s="hola"
print mitad(s) #produce un error de tipo


Conclusión

Sacar conclusiones es complicado. Valorar si es mejor tal o cual cosa es esfuerzo perdido... para según que aplicaciones o incluso partes de una misma aplicación puede venir mejor una y para otras no.

Valóralo tú mism@ y saca tus propias conclusiones: será el mejor criterio que puedas tener ;-)