¿Hacia dónde van los Lenguajes de Programación?

Yo aprendí a programar usando una microcomputadora Timex Sinclair 2068 a mediados de los años ochenta. Esta maquinita tenía 72 KB de memoria, usaba una grabadora convencional como dispositivo de almacenamiento secundario, y venía de fábrica con un intérprete de Sinclair BASIC. Este dialecto de Basic usaba números de línea frente a cada enunciado. Había que usar la instrucción GOTO para realizar repeticiones, GOSUB para llamar subrutinas, y todas las variables eran globales. Comparado con las herramientas que tenemos disponibles hoy en día, este era un ambiente muy primitivo, pero aún así podía pasar largas horas programando felizmente ese aparatito.

Muchas cosas han cambiado en las últimas dos décadas. Sin embargo, los avances en el campo de los lenguajes de programación han sido más una evolución que una revolución.

En las siguientes secciones describo brevemente las tendencias más importantes que, desde mi punto de vista, marcarán las pautas en el diseño de los lenguajes computacionales que estaremos utilizando en los años por venir.

Programación funcional
La programación funcional es un estilo de programación basado en la utilización de funciones matemáticas. El cálculo lambda, desarrollado por el matemático norteamericano Alonzo Church en la primera mitad del siglo veinte, es la base teórica de este estilo de programación. Lisp, Haskell, ML y Erlang son ejemplos de lenguajes funcionales.

Las operaciones de cómputo en la programación funcional se llevan a cabo a través de la evaluación de expresiones que producen valores y que están libres de efectos secundarios. Por el contrario, en el estilo de programación imperativa, al cual pertenecen la mayoría de los lenguajes convencionales, el énfasis está en ejecutar enunciados que precisamente producen efectos laterales. En este caso, el principal efecto lateral es la mutación explícita de los valores contenidos en memoria.

Las variables en los lenguajes funcionales son parecidas a las variables de álgebra. Esto es, una variable representa un valor inicialmente desconocido, pero una vez que se conoce, este ya no cambia. En contraste, en los lenguajes imperativos una variable es simplemente el nombre de una localidad de memoria cuyo contenido puede ser arbitrariamente leído y/o modificado. Gracias a que las variables son asignables una sola vez, los programas funcionales cuentan con una propiedad conocida como transparencia referencial. Se dice que una expresión es referencialmente transparente si puede ser remplazada por su valor, pero sin alterar los resultados producidos por el programa. La transparencia referencial es importante porque permite al programador, o al traductor del lenguaje, razonar sobre el comportamiento del programa. Este tipo de razonamiento es útil para probar que un programa es correcto, optimizar código a través de cachés, eliminar sub-expresiones comunes, simplificar algoritmos, e incluso parelelizar la evaluación de sub-expresiones.

Los lenguajes funcionales han introducido importantes conceptos que han sido posteriormente incorporados en muchos otros lenguajes. Uno de los más notables es el referente a la recolección de basura, en donde el ambiente de ejecución, y no el programador, es responsable de determinar cuando cierto objeto de memoria ya no es usado por el programa y por lo tanto puede ser reutilizado en alguna otra parte. Lisp fue el primer lenguaje que usó esta técnica, ocurriendo esto a finales de los años cincuenta. Sin embargo, todavía a inicios de la década de los años noventa había poco respeto por parte de la industria de software hacia los lenguajes que hacían uso extensivo de recolección de basura, debido principalmente al costo de ejecución en el que se incurre. Gracias a la atención que generó Java en la última década, hoy en día prácticamente todos los lenguajes considerados “modernos” incorporan recolección de basura.

Otros conceptos importantes en que los lenguajes funcionales han sido pioneros incluyen: funciones como objetos de primera clase, cerraduras léxicas, recursión, tipos dinámicos, inferencia de tipos, y meta-programación.

Lenguajes dinámicos
Un lenguaje de programación dinámico es un lenguaje de alto nivel que lleva a cabo en tiempo de ejecución muchas acciones que otros lenguajes típicamente llevan a cabo en tiempo de compilación. Estas acciones incluyen cosas como agregar y evaluar código, modificar el sistema de tipo de datos, añadir propiedades a objetos, etcétera. En esta categoría de lenguajes están Lisp, Smalltalk, Tcl, JavaScript, Python, Ruby, Perl, PHP y Groovy. Dada su naturaleza, los lenguajes dinámicos son normalmente interpretados, aunque sí existen compiladores para algunos de ellos.

Recientemente mucha gente ha tomado interés en los lenguajes dinámicos. Una de las principales razones es el incremento en la productividad que se logra al usarlos. Por ejemplo, es común que un programa escrito en Ruby o Python requiera entre 2 a 10 veces menos código que su equivalente en lenguaje C o Java. Usualmente un programa que es más pequeño toma menos tiempo en escribirse, es más fácil de comprender y modificar, y contiene menos errores que uno de mayor tamaño. Claro, esto no viene sin algunos inconvenientes. No es inusual que un programa escrito en un lenguaje dinámico llegue a ser decenas o incluso hasta centenas de veces más lento que un programa escrito en un lenguaje convencional compilado. Para cierto tipo de aplicaciones esto puede ser irrelevante.

Por ejemplo, imaginemos que tengo un cierto problema que debo resolver escribiendo un programa. Supongamos que si lo escribo en Python me llevaría dos horas, pero si lo hago en lenguaje C me llevaría diez. Por otro lado, supongamos que si ejecutara el programa en Python, éste tardaría cinco segundos mientras que el que está escrito en C tardaría 0.01 segundos. Bajo estos escenarios hipotéticos, tardaría cinco veces más en escribir mi programa en C en lugar de Python, pero al momento de ejecución el programa de C sería 500 veces más rápido. Entonces, ¿qué escenario me ofrece el mejor beneficio? Cuando una computadora costaba millones de dólares, el costo del tiempo de un programador era desdeñable. Pero hoy en día es generalmente al revés. Si mi programa hipotético sólo va a ser ejecutado una vez por semana, me conviene más usar un lenguaje en donde pueda ser más productivo y donde el tiempo de ejecución es prácticamente insignificante. Por otro lado, si el programa lo van a usar miles de usuarios varias veces por día, seguramente debo reconsiderar mis prioridades.

Existen también ocasiones cuando hay factores externos al programa mismo que constituyen cuellos de botella que difícilmente se pueden eliminar con la elección del lenguaje de programación a usar. Por ejemplo, el desempeño de una aplicación Web puede estar sujeto al ancho de banda de los clientes, el tiempo de respuesta del manejador de base de datos o los servicios Web que se utilizan, etcétera. Si estos factores corresponden a la mayor parte del tiempo de ejecución de la aplicación, y a menudo así ocurre, entonces el beneficio de usar un lenguaje más rápido es prácticamente imperceptible.

Otra desventaja que algunas personas han señalado con los lenguajes dinámicos es el relativamente limitado soporte que existe para ellos en las herramientas de desarrollo, particularmente los IDEs. Esto se debe a que la mayoría de los lenguajes dinámicos no usan declaraciones explícitas de tipos, y esto complica significativamente cosas como la facilidad de autocompletar código y la refactorización.

Muchos de los lenguajes dinámicos son considerados también como lenguajes de script, o guiones. Un lenguaje de script sirve como pegamento para combinar diversos componentes, los cuales pueden ser otros programas, utilerías del sistema operativo, o elementos de una interfaz gráfica de usuario. El término script hace alusión a los guiones que su usan en el cine o teatro. En el pasado, los lenguajes de script eran llamados batch languages (lenguajes de lotes) o job control languages (lenguajes de control de trabajos), y probablemente los ejemplos más representativos de éstos eran JCL en los equipos mainframe, los shell scripts de Unix, y los archivos .BAT de DOS. Hoy en día,este tipo de lenguajes se usan extensivamente para realizar tareas de mantenimiento de un sistema, facilitar la adecuación de la funcionalidad de alguna aplicación, enriquecer la interacción de usuario desde un browser, o simplificar la programación de páginas con contenido dinámico generadas por un servidor de web.

Programación paralela
La comercialización masiva de los primeros chips con múltiples núcleos (multi-core) en el año 2005 dio lugar a lo que se conoce como el fin del almuerzo gratis. En el pasado no muy lejano, un desarrollador podía escribir un programa sin preocuparse mucho sobre su desempeño, pues sabía que en relativamente poco tiempo el nuevo hardware correría su programa, sin modificación alguna, de manera más rápida (por eso el término almuerzo gratis). Sin embargo, las velocidades de los microprocesadores ya no se han estado incrementado como estábamos acostumbrados. La Ley de Moore establece que el número de transistores que se pueden retacar en un solo chip se duplica aproximadamente cada 18 meses. Esto típicamente se traducía en CPUs corriendo a más megahertz cada año. Sin embargo, este aumento en velocidad de reloj ya no es sostenible por cuestiones de calentamiento y consumo de energía. Esto no quiere decir que la Ley de Moore ya no se cumpla, pero ahora lo que están haciendo los fabricantes de microprocesadores es usar esos transistores adicionales para añadir más núcleos a los CPUs. Un núcleo es básicamente una unidad de procesamiento, que incluye registros, unidades de ejecución y memoria caché. Hoy en día, muchos servidores, equipos de escritorio y de cómputo portátil cuentan con CPUs con 2, 4 o más núcleos. Incluso empresas como Intel, IBM y Sun Microsystems ya están hablando de procesadores many-core, con decenas o centenas de núcleos para los años venideros.

Ahora bien, hay un problema muy grande: un programa no se beneficia del nuevo nivel de paralelismo inherente en los sistemas multi-core, a no ser que haya sido diseñado explícitamente para ello. Lamentablemente, la mayoría de los programadores no saben cómo escribir programas paralelos. Esto se debe probablemente a dos causas: 1) esta es una habilidad que sólo habían requerido la gente de nichos muy específicos que utilizan supercomputadoras, grids y clusters; 2) escribir programas paralelos correctos es considerablemente más difícil que escribir programas secuenciales, al menos usando la mayoría de las herramientas disponibles en la actualidad.

Desde hace un par de décadas han existido dos modelos de programación concurrente:

* Hilos con estado compartido. Se tiene una región de memoria compartida por dos o más hilos de ejecución. Para evitar que dos hilos entren en una condición de carrera (los hilos intentan modificar el mismo contenido de memoria al mismo tiempo) se debe usar algún mecanismo de candados para garantizar una exclusión mutua. Los candados pueden ser semáforos, monitores, etcétera. Sin embargo, el uso de candados conlleva a otro tipo de problemas potenciales como son los candados mortales (deadlocks) y la inanición.

* Paso de mensajes. Los procesos se comunican entre sí a través del envío y recepción de mensajes asíncronos. No hay memoria compartida entre los procesos, por lo tanto no se requieren candados.

Casi todos los lenguajes usados en la industria usan hilos con estado compartido. Debido a la complejidad inherente de este modelo, diversos lenguajes han sido extendidos con bibliotecas y/o directivas especiales que simplifican la escritura de programas paralelos. OpenMP, por ejemplo, es un API para C++ y Fortran que permite de manera relativamente sencilla paralelizar ciclos. El Task Parallel Library (TPL), disponible para C# 3.5, ofrece facilidades similares.

El paso de mensajes, por otro lado, ofrece un mayor nivel de abstracción ya que el programador no tiene que interactuar con las primitivas de bajo nivel asociadas normalmente con los hilos. El lenguaje Erlang soporta este modelo de programación y ha sido usado de manera extensiva en el dominio de las telecomunicaciones para alcanzar un alto grado de paralelismo. Un programa en Erlang que haga uso de cientos o hasta miles de procesos puede en principio tener una excelente escalabilidad en cuanto se ejecute en un sistema con un mayor número de núcleos.

Lenguajes multi-paradigmas
En los años ochenta Smalltalk era el lenguaje más representativo de la programación orientada a objetos. Pero para la mayoría de los programadores de esa época, Smalltalk era simplemente muy diferente a lo que estaban acostumbrados. La programación orientada a objetos comenzó a tomarse en serio en la industria hasta que los lenguajes convencionales fueron modificados para soportar dicho estilo de programación. Así surgieron C++ y Object Pascal, sólo por nombrar los más populares.

Este esquema de lenguajes que soportan más de un estilo o paradigma de programación sigue siendo la norma hasta nuestros días. Ruby y Python son lenguajes dinámicos y orientados a objetos, pero también tienen elementos que les permiten ser usados como lenguajes funcionales. Erlang es un lenguaje funcional, concurrente y distribuido. El lenguaje Oz soporta programación lógica, funcional, orientada a objetos, basada en restricciones, distribuida y concurrente.

Plataformas de programación Hoy en día, el desarrollo de software tiende a ser algo más enfocado a programar en una plataforma y no tan solo en un lenguaje. Es decir, tenemos ahora programadores y/o desarrolladores de web, de JEE y de .NET. Parece que la época del programador mono-lingüista está llegando a su fin. Por ejemplo, un desarrollador de web debe conocer varios lenguajes para hacer su trabajo, incluyendo HTML, CSS, JavaScript, y algún framework para usar Ajax como JQuery o Prototype, y todo esto sólo para la programación del lado del cliente; del lado del servidor probablemente requiera saber SQL, un framework para algún lenguaje de programación en particular, y un lenguaje de plantillas para la generación de contenido dinámico. ¿Por qué se requieren tantos lenguajes para desarrollar una aplicación de este tipo? Porque cada lenguaje tiene una función particular que los otros simplemente no pueden hacer, o sí lo pueden hacer pero de manera menos conveniente.

En la actualidad, las plataformas Java y .NET reflejan también la importancia de poder combinar lenguajes, y así ofrecer al desarrollador la opción de usar la mejor herramienta para el problema en cuestión. Por ejemplo, Scala es un lenguaje que corre en la plataforma Java, y que reúne lo mejor de muchos mundos. Es orientado a objetos y funcional, soporta el modelo de concurrencia por paso de mensajes. Usa un sistema de tipos estáticos pero con inferencia automática. Puede usarse como lenguaje de script e interactuar con código de Java existente.

JRuby, Jython y Groovy son lenguajes dinámicos diseñados también para simplificar y agilizar el desarrollo sobre la plataforma Java, sin perder la facilidad de operación con código de Java. Es claro ahora más que nunca que lo valioso de la tecnología Java es su plataforma (la máquina virtual y su extensa biblioteca) y no tanto el lenguaje de programación en sí. Algunos han afirmado que Java se ha convertido en el nuevo Cobol. A mi me parece mas bien que Java es el nuevo C. Así como en Unix la infraestructura básica (núcleo, utilerías, bibliotecas) está escrita en lenguaje C, y todo se une con scripts y aplicaciones escritas en lenguajes de más alto nivel (tales como Perl, Tcl o Python), así también podría ocurrir con Java y los otros lenguajes ya mencionados.

Algo similar ocurre en la plataforma .NET. De hecho, a diferencia de la máquina virtual de Java, que fue diseñada específicamente para ese lenguaje de programación, la máquina virtual de .NET, conocida como el Common Language Infrastructure (CLI), fue diseñada desde el principio para soportar múltiples lenguajes y sus interacciones. Aunque C# es el caballito de batalla del CLI, existen decenas de lenguajes diseñados para esta plataforma. Por ejemplo, F# es un lenguaje funcional y orientado a objetos, derivado principalmente del lenguaje ML. También hay implementaciones de Python y Ruby para el CLI, llamadas respectivamente IronPython e IronRuby.

Conclusión
En la actualidad hay muchas cosas interesantes ocurriendo en el área de lenguajes de programación. Después de años de estar estudiando diferentes lenguajes, me resulta claro que difícilmente encontraremos un lenguaje único y perfecto. Lo mejor que podemos hacer es sacarle el mayor provecho a lo que tenemos disponible hoy en día, y estar en la mejor disposición de aprender las nuevas herramientas que vayan apareciendo.

Acerca del autor
Ariel Ortiz Ramírez es profesor de planta del Departamento de Tecnologías de Información y Computación del Tecnológico de Monterrey, Campus Estado de México. Desde 1988 ha estado impartiendo una gran variedad de cursos relacionados con programación en donde ha utilizado los lenguajes Basic, Pascal, C, C++, C#, Java, JavaScript, Scheme, Prolog, Python, Ruby, Erlang y diversos ensambladores. Sus principales áreas de interés son los lenguajes de programación, la educación en ciencia de la computación y el software libre. www.arielortiz.com