sábado, 18 de mayo de 2019

Paradigmas y tipos de lenguajes informáticos (2 de 3)

Segunda parte de las clasificaciones de los lenguajes informáticos, esta vez vamos separarlos por lenguajes interpretados y compilados y también según el tipado.

Lenguajes interpretados o compilados



Lenguajes compilados


Los lenguajes compilados son aquellos en los que el código fuente pasa por una seria de transformaciones hasta que se genera un código máquina (dicho proceso, se llama compilación). El código maquina es el "producto final", el producto que se va a distribuir, y ejecutar.

Características

  • Están compilados para una arquitectura hardware en particular y para unos sistemas operativos en concreto, esto hace que, si bien su velocidad sea la más óptima posible, sean dependiente de dichas arquitecturas, es decir si queremos ejecutar nuestro software en diversas plataformas, debemos compilarlo varias veces.
    Aunque pudiera parecer "sencillo" compilarlo varias veces, una para cada plataforma, descubríamos ciertamente que no lo es. Entre los problemas a encontrar están los siguientes:
  • Los procesadores de 32 y 64 bits, tienen distintos tamaños para sus tipos de variables, por ejemplo un entero en el compilador de 32 bites pude medir 2 byte y en el de 64bits, puede medir 4, lo cual provoca caer fácilmente en problemas de memoria si dependemos del tamaño de las variables. ara tomar decisiones. Lo anterior también puede ocurrir entre distintas versiones de un mismo lenguaje en varios compiladores.
  • Las librerías y utilerías de una plataforma, no tienen por qué estar en otra, con lo que nos obligaría a reestructurar completamente nuestro código, según tengamos dependencias de estas.

Es ciertamente una forma de no compartir nuestro código fuente sin necesidad, pero debemos considerar que existen herramientas de descompilación, que nos pueden dar una idea aproximada de cómo era nuestro código fuente originalmente.

Quizás el mas típico representante de los lenguajes compilados es C.

Lenguajes interpretados


Ciertamente no se puede ejecutar, en ningún caso, un código fuente directamente, es decir la transformación de código a fuente a código máquina, siempre se tiene que dar, pero en este caso se da cada vez que se ejecuta nuestro sistema, puesto que el producto final es el código fuente. El código fuente es lo que distribuiremos y es lo que se configurara en las maquinas destino.

Los lenguajes interpretados son bastante populares en la actualidad, lo que ha impulsado que haya multitud de librerías y frameworks disponibles en internet, además de mecanismos para consumirlos de forma sencilla y directa.

Debido a que la distribución de código fuente es indispensable, son lenguajes propicios tanto para el software libre, así como para aplicaciones de servidor.

Las principales características que encontramos son:

  • Homogeneidad ente distintas plataformas: Debido a que el código fuente es portable, este se ejecuta de igual forma (sin modificaciones), entre los distintos escenarios posibles.
  • Menor necesidad de versiones concretas de sistemas operativos, o componentes externos en particular. Prácticamente son sistemas que se enlazan dinámicamente a dependencias, y generalmente es necesario usar el nombre del componente a usar, sin tener ningún tipo de liga compleja basado en compatibilidad binaria, o semejantes.
  • Debido a que le código, por necesidad, está disponible, siempre es posible hacer un diagnóstico basado en este.
  • Facilidad de despliegue, debido a la sencillez para resolver dependencias, casi siempre consiste en copiar los archivos requeridos a la ruta indicada.
  • Clásicamente la ejecución es más lenta que en los sistemas compilados, pero los lenguajes interpretados modernos tienen mecanismos para garantizar que esta lentitud es solo apreciable la primera vez que se ejecuta el sistema, disminuyendo el tiempo drásticamente en ejecuciones posteriores.

Algunos ejemplos son:

  • PHP
  • Ruby
  • Python


Lenguajes de compilación intermedia


Algunos lenguajes caen en un punto intermedio, son lenguajes compilados, siendo el producto final un ejecutable en código máquina, pero dicho código maquina no es de una plataforma en particular, sino de una máquina que no existe físicamente. Es lo que se conoce como una máquina virtual.

Este código intermedio debe ser interpretado por cada plataforma destino para poder ejecutarse. ¿Cuáles la ventaja que tenemos entonces con este tipo de lenguajes?


  • Sistemas Multiplataforma
  • Homogeneidad
  • Rapidez aceptable

Al programar para una máquina virtual, nuestro código maquina no está ligado a ninguna plataforma en particular, no tiene dependencias particulares de hardware. El tamaño en memoria de las variables básicas, y su estructura esta definida y siempre es la misma (independientemente si el procesado es de 32 o 64 bits). Adicionalmente el código intermedio de la máquina virtual esta tan próxima a los modelos tradicionales de máquinas físicas que la traducción es bastante rápida. Así tenemos los mejor de los lenguajes compilados y los lenguaje interpretados.

Lenguajes de este tipo son por ejemplo C# y Java



Según el "Tipado"


Todas la variables en memoria de un programa, tiene evidentemente un tipo, es decir son cadenas de texto, numero enteros, decimales, fechas, o cualquier otro tipo de estructura o tipo.

Ahora bien, las variables son espacios de memoria, una secuencia de bytes, lo que le da identidad realmente es lo que "creemos" que hay en ese espacio de memoria. El "creemos", hace referencia al tipo de variable que esta apuntado a ese espacio de memoria, si el tipo de variable que está apuntándolo es un entero, supondremos que el espacio de memoria al que apunto es un entero, si es una cadena supondremos que es una cadena.

El como el lenguaje permite gestionar el "tipo" de las variables es lo que llamaremos su "tipeado", según mas restricciones tenga, mayor y más fuerte será el tipeado, una tipeado mayor nos garantizara que solo variables de un tipo apunten a espacios de memoria donde este tipo, por ejemplo garantiza que si nuestra variable es un entero, solo pueda apuntar a un espacio de memoria que haya un entero.


Lenguajes estáticos y dinámicos



  • Los lenguajes estático son aquellos en los que una vez definido el tipo de una variable, dicha variable siempre será del mismo tipo, no pueden apuntar en un momento a un entero y al siguiente a un cadena de texto por ejemplo, esto es propio de los lenguajes compilados.
  • Los lenguajes dinámicos, son aquellos en que el tipo al que puede apuntar una variable puede cambiar, por ejemplo en este caso en un momento puede apuntar a un entero y al siguiente a una cadena de texto, este comportamiento es típico de los lenguajes interpretados.

Hay que tener en cuenta un punto, algunos lenguajes orientados a objetos (sobre todo los modernos), tienen un clase antecesora común para todas las demás clases, generalmente se llama Object. Un objeto de tipo Object, pudiera apuntar a cualquier variable, en cualquier momento. Esto nos puede hacer pensar que el lenguaje es dinámico, pero no es cierto, simplemente están involucrados mecanismos de herencia pero un objeto de tipo Object, apunta a un Object, para usarlo como un entero, o una cadena de texto, obligatoriamente debemos hacer un cast.

Lenguajes de tipado débil


Es cuando se conoce de que tipo es una variable, pero es posible hacer que dicha variable apunte a un espacio de memoria, donde no este un valor de dicho tipo, esto puede generar un error o no en tiempo de ejecución (aunque lo más problema es que por lo menos genere un comportamiento extraño y no deseado).

Lenguajes de tipeado fuerte


Los lenguajes de tipeado fuerte, son aquellos que tienen un estricto control sobre el tipo de variable y el tipo de contenido a la que están apuntando, si están apuntado a un tipo incorrecto el programa no compilaría, si están en tiempo de ejecución se da la circunstancia que una variable apuntara a un tipo incorrecto, el programa de detendría generando un error (en vez de los tipados débiles, que continuaría haciendo cosas "raras", hasta que el fallo fuera demasiado grande).

JavaScript, tipado débil y dinámico



Analicemos el siguiente código en JavaScript, para ver cómo se comportan las variables (que son de tipado débil y dinámicas). El código es simple suma:

Ejemplo JavaScript
 
1
2
3
4
5
6
// Las variables no tienen un tipo, se declara con var
var a=1;
var b=2;
var c= a + b;
//En este caso debe C debe valer 3
console.log(c)
 
 
// Las variables no tienen un tipo, se declara con var
var a=1;
var b=2;
var c= a + b;
//En este caso debe C debe valer 3
console.log(c)
La salida será 3.

Ahora bien, por error podemos asignar el valor "hola" a la variable "a" y volver a realizar la suma, esto no generara ningún error (ni al compilar, ni la ejecutar), pero si una circunstancia inesperada

Ejemplo JavaScript con errores
 
01
02
03
04
05
06
07
08
09
10
11
12
// Las variables no tienen un tipo, se declara con var
var a=1;
var b=2;
var c= a + b;
//En este caso debe C debe valer 3
console.log(c)
//Ahora bien imaginemos que asignamos el valor "hola" a "a"
//Nos permite hacerlo sin problemas
a="hola "
c= a + b;
// En este caso el resultado sera "hola 2"
console.log(c)
 
 
// Las variables no tienen un tipo, se declara con var
var a=1;
var b=2;
var c= a + b;
//En este caso debe C debe valer 3
console.log(c)
//Ahora bien imaginemos que asignamos el valor "hola" a "a"
//Nos permite hacerlo sin problemas
a="hola "
c= a + b;
// En este caso el resultado sera "hola 2"
console.log(c)
La salida será hola 2 lo cual posiblemente no tenga ningún sentido.

C, tipado débil y estático




El siguiente código en C, muestras un ejemplo de un lenguaje de tipado débil y declaración de variables estáticas, esto es que forzosamente debo declarar el tipo de las variables, pero estas pueden a apuntar a espacios de memoria, que no tuvieran ese tipo en particular.

El código del siguiente ejemplo es válido, compila y no generar errores al ejecutarse, sin embargo, estoy convirtiendo enteros a floats, y string, sin que posiblemente me dé cuenta de ello, por lo que pudiera tomarse como un error de dedo y los resultados son "extraños" (nótese que nunca se interrumpe el programa).

Ejemplo C con código incorrecto
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
int main()
{
    int a=120;
    //Imprimo a como un numero, es valor es 50;
    printf ("El valor de a es '%d' \n",a);
     
    //imprimo a como un float se imprime un valor
    //pero no es 50,y no hay ningun fallo
    printf ("El valor de a es '%f'\n",a);
     
    //Lego el valor de a del teclado como cadena de texto
    printf ("Introducta el valor de a: ");
    scanf("%s",&a);
    printf ("El valor de a es '%d' \n",a);
     
}
 
 
int main()
{
    int a=120;
    //Imprimo a como un numero, es valor es 50;
    printf ("El valor de a es '%d' \n",a);
    
    //imprimo a como un float se imprime un valor
    //pero no es 50,y no hay ningun fallo
    printf ("El valor de a es '%f'\n",a);
    
    //Lego el valor de a del teclado como cadena de texto
    printf ("Introducta el valor de a: ");
    scanf("%s",&a);
    printf ("El valor de a es '%d' \n",a);
    
}
La salida del código es:
El valor de a es '120'
El valor de a es '0.000000'
Introduzca el valor de a: hola
El valor de a es '1634496360'

A pesar que no tiene sentido, el programa funciona y continua su ejecución, sin percibir nada extraño.

C#, tipado fuerte y estático




C# es un lenguaje de fuerte tipeado y estático, es decir las variables tienen un tipo específico y solo pueden apuntar a un valor de dicho tipo.

Veamos el siguiente ejemplo_

Ejemplo C# con código incorrecto
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
 
namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int a=20;
            string b=(string) a;
        }
    }
}
 
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int a=20;
            string b=(string) a;
        }
    }
}
Genera el siguiente error:

(16:22) Cannot convert type 'int' to 'string'

Esto es porque no se puede convertir un entero a una cadena de texto, un variable de tipo string, no puede apuntar a un valor de tipo entero.

Para hacerlo posible, habría que convertir explícitamente el valor entero a una cadena de texto, básicamente esta conversión consiste en crear una nueva dirección de memoria donde este una cadena de texto y no un entero (con lo que tendríamos dos direcciones de memoria, la del entero y la cadena de texto).

Ejemplo C# correcto
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
 
namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int a=20;
            string b=a.ToString();
        }
    }
}
 
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            int a=20;
            string b=a.ToString();
        }
    }
}
A veces pensamos que los lenguajes en los que especificamos explícitamente el tipo de la variable son estáticos y los que no dinámicos, pero esto no es siempre cierto, veamos el siguiente código:

Ejemplo C# incorrecto
 
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
 
namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var a=20;
            a="hola";
        }
    }
}
 
 
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;

namespace Rextester
{
    public class Program
    {
        public static void Main(string[] args)
        {
            var a=20;
            a="hola";
        }
    }
}

Nos generara el error

(16:15) Cannot implicitly convert type 'string' to 'int'

Aquí usamos la cláusula var, para crear variable y le asignamos el valor "20" esto lo convierte en una variable de tipo entero, y nunca podrá cambiar su tipo, ni apuntar a otro espacio de memoria donde no haya un entero, las sentencia var a=20, equivale a int a=20, la declaración del tipo es estática y en tiempo de compilación.

Ruby, tipado fuerte y dinámico




Es común suponer que si un lenguaje es interpretado debe ser no de tipado débil, pero esto no siempre es así, por ejemplo veamos el siguiente código de Ruby (lenguaje interpretado):

Ejemplo C# incorrecto
 
1
2
3
a=10
b="2"
c=a+b
 
 
a=10
b="2"
c=a+b

Nos generar el error:

String can't be coerced into Integer (repl):3:in `+' (repl):3:in `<main>'

Porque no es posible convertir explícitamente la cadena de texto en un entero, para corregir el código, hacemos las siguientes modificaciones:

Ejemplo C# incorrecto
 
1
2
3
a=10
b="2"
c=a+b.to_i
 
 
a=10
b="2"
c=a+b.to_i
Esta vez nos da el resultado 12.

Como vemos para iniciar una variable no hay ninguna palabra reservada, solo debemos asignarle un valor, pero Ruby si tiene ligado el tipo de la variable a su contenido, de forma que no se pueden convertir directamente, ni usar un tipo como otro, sin sé que se indique.




Si te ha gustado la entrada, ¡Compártela! ;-)

Nota: Puedes encontrar todo el código fuente de este artículo en https://github.com/jbautistamartin/ParadigmasTiposLenguajes