La Torre de Babel informática de los lenguajes de programación
En el artículo anterior introdujimos la idea de lenguaje de alto nivel, y del rol del compilador como traductor entre esos lenguajes y el lenguaje máquina, que es el que entiende el procesador. Pensar en alto nivel no quiere decir más que evitar tener que pensar hasta en el detalle más nimio de nuestro programa, usando abstracciones como variables o funciones, más cercanas al modo de pensar humano. Pero como veremos en esta entrega, encima de estas abstracciones básicas existen otras muchas, y los diferentes lenguajes proveen diferentes subconjuntos de las mismas.
Los lenguajes de programación son una creación puramente humana, que en cierto modo reflejan la forma de pensar de sus creadores. No es por ello extraño que haya cientos (sino miles) de lenguajes, agrupados en una decena de familias diferentes, según el modo en el que los programas se estructuran. No es lo mismo pensar en un programa de forma lineal, que pensar en un programa como una serie de eventos del exterior al que dar respuesta. Un lenguaje del primer tipo resultará pues más conveniente para tareas con un principio y final muy definido, como calcular el tiempo que hará mañana; mientras que la segunda abstracción resulta más útil para describir el funcionamiento de una aplicación móviles, donde podemos tocar diferentes elementos en la pantalla.
Curiosamente, si uno escucha las discusiones sobre lenguajes, muchas de las diferencias entre ellos son meramente estéticas. Por ejemplo, para expresar “la variable x toma el valor 5” ¿es mejor escribir “x = 5” (como se hace en Java o en C), “x := 5” (como se hace en Pascal) o “x < – 5” (como se hace en R)? Tan encendidas son estas discusiones que un famoso diseñador de lenguajes, Philip Wadler, enunció una ley que venía a decir que se gastaba exponencialmente más tiempo discutiendo sobre la sintaxis de los lenguajes (esto es, la forma en la que sus conceptos se trasladan en letras) que en la semántica (esto es, las ideas que subyacen detrás de lo que escribimos).
Pero volvamos de nuevo a la enjundia del asunto, a las diversas abstracciones que nos ofrecen las diferentes familias de lenguajes. Aunque se pueden categorizar de diversas formas y tan finamente como queramos, existen 4 grandes familias o paradigmas comúnmente aceptadas: lenguajes imperativos, lenguajes orientados a objetos, lenguajes funcionales y lenguajes lógicos; estos dos últimos se suelen englobar bajo el epígrafe de lenguajes declarativos.
Lenguajes imperativos y orientados a objetos
Un programa escrito en un lenguaje imperativo es básicamente una secuencia de comandos u órdenes (de ahí el nombre) que se ejecuta de principio a fin. Las órdenes más básicas son modificar el valor de una variable o llamar a una función, las abstracciones más simples sobre las que hablamos en la entrega anterior. A estas órdenes básicas los lenguajes imperativos añaden dos elementos:
- Condicionales: maneras de redirigir el flujo de la ejecución dependiendo de un valor o una propiedad. Por ejemplo: si el cuadro de texto del nombre esta vacío (condición) entonces mostrar un mensaje de error (orden).
- Bucles: maneras de repetir una serie de órdenes. Por ejemplo: por cada número del 1 al 10, muestra ese número en la pantalla. Nótese que no es necesario conocer a priori el número de repeticiones, sino que puede depender de otra condición. Por ejemplo: mientas que el usuario no pulse S o N, muestra el mensaje “pulse S o N” en pantalla.
Dada la sencillez de estas abstracciones, muchos lenguajes de otras familias también las proveen. Pero dentro de los lenguajes imperativos puros, podemos destacar Fortran (nacido en 1954), Basic (1964), Pascal (1970), C (1972), o Python (1990). Una gran ventaja es que estas abstracciones son muy cercanas o muy fáciles de traducir al lenguaje máquina, de ahí que mucho del software de alto rendimiento se siga escribiendo en este paradigma.
Sin embargo, según los programas crecían en tamaño y complejidad, empezaron a necesitarse nuevas abstracciones que separasen al programador aún más de la máquina subyacente. A mediados de la década de 1960 se introdujeron las primeras ideas de la orientación a objetos, en 1970 lenguajes como Simula o Smalltalk ya eran puramente de este paradigma, pero la gran eclosión sucedió a partir de 1985 con C++, convirtiéndose en el paradigma dominante. Casi cualquier nuevo lenguaje “mainstream” desde entonces — como Java, Ruby o C# — es un lenguaje orientado a objetos.
La idea fundamental de este paradigma es que el código se articula a través de una multitud de objetos (a veces llamados actores) que trabajan independientemente y se comunican entre sí. Si queremos imprimir una cuenta de un restaurante, nos comunicaremos con un “objeto cuenta” al que le diremos qué se ha consumido, y este objeto de forma independiente se encargará de realizar la suma del total y comunicarse con otro “objeto impresora” para imprimir el recibo. La mayor ventaja de este paradigma es su modularidad: diferentes equipos pueden trabajar independientemente en partes de un programa, y sólo tienen que ponerse de acuerdo sobre la forma en la que estos objetos o actores se comunicarán entre sí.
Sobre esta idea común, la mayor parte de los lenguajes orientados a objetos introducen el concepto de subtipado, que es una forma de abstraerse de las peculiaridades de cada objeto. El ejemplo más usado es el de los animales: a veces necesito comunicarme con un “objeto perro” o un “objeto gato”, pero otras veces me es suficiente con un “objeto animal de compañía”. En orientación a objetos podemos declarar que un perro es un animal de compañía, y usar el primero allí donde se requiera el segundo sin más problema. Si quiere un ejemplo un poco más informático: el “objeto impresora” al que nos referíamos antes puede ser subtipo de un “objeto enviador-de-información” del que otro subtipo sea “objeto envío-por-correo” que manda el recibo por correo electrónico.
Lenguajes declarativos
Lo opuesto a dar órdenes al ordenador es intentar explicar al ordenador a donde quieres llegar y que éste se las apañe para darte el resultado. Esa es al menos la filosofía de los lenguajes declarativos: dar un lenguaje de muy alto nivel donde el programador se encargue más de expresar el problema que tiene en sí, y no tanto de la forma en la que el programa se ejecuta. La desventaja ha sido tradicionalmente la velocidad de estos lenguajes, ya que es una ardua tarea para el compilador traducir todo a una serie de órdenes sencillas, aunque esta desventaja se ha reducido considerablemente en los últimas décadas.
Los lenguajes lógicos miran al mundo como una serie de “hechos” que se toman como ciertos y una serie de “reglas” para inferir información de esos hechos. Un ejemplo más concreto es la descripción de relaciones familiares entre padres e hijos:
Junto a una regla que dice “Fulanito es abuelo de Menganito” si “Menganito es hijo de Pepito y Pepito es hijo de Fulanito”. En un lenguaje lógico podemos hacer ahora preguntas cerradas como “¿Julio es abuelo de Quique?”, a lo que nos responderá “no”; o preguntas abiertas como “¿Julián es abuelo de Nieto?”, a lo que nos responde “Nieto = Quique, Nieto = Julio”, pues esos son los dos valores de “Nieto” para los que la respuesta es afirmativa.
Estos lenguajes fueron muy populares en las primeras oleadas de inteligencia artificial, ya que se consideraba una buena representación de la información y sus reglas. Prolog, casi el único superviviente actual de este paradigma en su estado puro y nacido en 1972, continúa siendo objeto de investigación. En la actualidad, cuando se necesita un sistema de reglas como el que nos da la programación lógica, se suele integrar como parte de un programa escrito en otro paradigma.
Lenguajes funcionales
La otra gran familia declarativa es la formada por los lenguajes funcionales. Hemos hablado de que una de las abstracciones por excelencia es la de dar un nombre a un valor en un programa. En la mayor parte de los lenguajes, cuando decimos “valor” nos referimos a un dato, como un número, la información de una persona, etcétera. En un lenguaje funcional la barrera entre variable y función se difuminan, y podemos manipular a una función (esto es, una secuencia de instrucciones para el ordenador) como cualquier otro valor. Por ejemplo, imaginemos que tenemos una lista de personas y queremos realizar una operación sobre cada una de ellas. En un lenguaje imperativo tenemos que escribir un bucle nuevo por cada operación que tengamos que hacer. En programación funcional lo que tenemos es una operación que recibe otra función y la ejecuta sobre cada elemento de la lista. Esto nos evita tener que escribir un bucle cada vez.
La programación funcional es casi tan antigua como la informática misma. Deriva de un modelo de computación denominado cálculo lambda, descrito en la década de 1930 por Alonzo Church. Lisp, el segundo lenguaje de alto nivel de la historia (1958), es un lenguaje funcional. Algunos ejemplos más actuales son Erlang (1986), Haskell (1990), Racket (1995), F# (2005), Clojure (2007). Además, hay una tendencia a incorporar elementos de programación funcional en lenguajes ya existentes, como Java, o mezclar la programación orientada a objetos y la funcional, con ejemplos como Scala (2004) o Swift (2014).
Lenguajes específicos del dominio
Hasta el momento todos los lenguajes de los que hemos hablado son de propósito general. Esto quiere decir que, con sus más o sus menos, estos lenguajes permiten expresar cualquier necesidad computacional que tengamos. Hay otros lenguajes que se centran en dar solución a problemas de un área (o dominio) determinada, excluyendo de forma deliberada otros usos. Por ejemplo, la página web que estás leyendo ahora mismo está escrita en un lenguaje denominado HTML, centrado únicamente en la representación de texto e imágenes con posibles vínculos a otras páginas (muy probablemente si pinchas con el botón derecho haya alguna opción de “ver código” que te muestra el HTML).
Una necesidad importante de los sistemas de información es consultar y modificar bases de datos. De nuevo, nos encontramos con un lenguaje específico del dominio, SQL, orientado a expresas esas consultas de la forma más concisa posible. Un último ejemplo es también de sobra conocido: las hojas de cálculo. Cada vez que escribimos una fórmula en una celda, como “=SUMA(A1:A20)”, estamos escribiendo en un pequeño lenguaje de fórmulas matemáticas.
Desde fuera, puede parecer que “escribir un programa” es algo que se hace de una manera concreta. Pero como hemos visto, en el medio siglo de vida de la informática se han creado multitud de lenguajes de programación, que ofrecen una gran diversidad de formas de pensar sobre los problemas a resolver.