lunes, marzo 13, 2006

lenguajes de programación (ahora en serio)

Advertencia: este artículo tiene un contenido técnico. Antes de leerlo consulte con su farmacéutico, psicólogo o párroco habitual.


Soy aficionado a coleccionar lenguajes de programación. Me gusta aprenderlos, cacharrear con ellos, compararlos, buscar el BWTDI (Better Way To Do It).

Muchas veces uno se encuentra en foros preguntas del tipo "¿cuál es el mejor lenguaje de programación?", que siempre desembocan en tremendos flames del tipo "mi mierda huele mejor que la tuya" y si buscas en google "programming language comparison" lo que más abundan son benchmarks numéricos o propaganda barata.

Hay una infinidad de factores a considerar (incluyendo la cara de los diseñadores) a la hora de valorar un lenguaje de programación, y muchos de ellos son excluyentes, de modo que lo que puede ser un pro en uno se puede convertir en un contra en otro.

Así que voy a intentar definir qué cosas importan cuando vayamos a escoger un lenguaje de programación para un proyecto, o simplemente para aprenderlo como pasatiempo:
  • Paradigma: determina radicalmente la forma de abordar los problemas. Normalmente uno se decanta por el paradigma al que está más acostumbrado, ya que todos los paradigmas prácticos son Turing-Completo, es decir, cualquier computación realizable se puede llevar a cabo con cualquiera de ellos. La elección del paradigma adecuado para un problema puede reducir drásticamente el tiempo, el coste y la facilidad con la que se llega a la solución, aunque si no se tiene experiencia con el paradigma en cuestión, puede llegar a ser contraproducente cambiar del que más se domina.
    Existen muchas formas de clasificar los paradigmas, ya que muchos se solapan y depende del nivel de detalle al que se quiera llegar. He hecho un diagrama que los agrupa desde mi punto de vista:Free Image Hosting at www.ImageShack.us, para obtener información más detallada: Programmming Paradigm (Wikipedia).
    • Imperativo: es un paradigma mayor, consiste en que se ejecutan instrucciones que modifican el estado del programa.
    • Top-Down: consiste en especificar el programa como una secuencia de instrucciones que se ejecutan secuencialmente, con la posibilidad de incluir saltos a otras partes del programa. A pesar de que ya se encuentra en desuso (el motivo en la carta de Dijkstra Goto Statement Considered Harmful), es el paradigma nativo de la arquitectura Von Neumann. Ejemplos: Ensamblador, BASIC.
    • Estructurado: el programa se divide en bloques independientes (procedimientos y funciones) y el flujo se controla mediante estructuras de control en lugar de gotos. Fue un gran avance respecto a la programación top-down ya que permitía empezar a reutilizar código y facilitaba enormemente la comprensión de los programas. Ejemplos: C, Pascal.
    • Orientado a Objetos: los datos y las operaciones que actúan sobre ellos se agrupan en "objetos" (hablar en profundidad está lejos del propósito de este post, además, el que ha llegado hasta aquí seguro que ya sabe lo que son los objetos). Es un siguiente paso en la programación estructurada, que permite empezar a desarrollar componentes completos reutilizables de manera fácil. Ejemplos: Java, D.
    • Declarativo: es el otro gran paradigma. Se trata de especificar los programas como una descripción abstracta (de alto nivel) la solución a un problema en lugar de detallar paso por paso cómo llegar hasta ella. Este paradigma es por lo tanto más sofisticado que el imperativo, ofrece muchas ventajas al programador, como la posibilidad de comprobar la corrección formal de los programas, pero tiene como inconveniente que el rendimiento suele ser peor.
    • Funcional: en este paradigma se intenta imitar el lenguaje de las matemáticas, expresando toda la lógica del programa como funciones y composición de las mismas. Tiene algunas ventajas importantes, como la ausencia de efectos secundarios (una de las mayores fuentes de quebraderos de cabeza de los programadores), suele permitir trabajar con tipos infinitos de forma eficiente y cómoda y presenta muchas soluciones elegantes a problemas tediosos que se presentan en la programación estructurada. Ejemplos: Haskell, Erlang.
    • Lógico: en este paradigma se definen reglas lógicas y luego un motor de inferencia lógico permite extraer conclusiones o responder a preguntas planteadas al sistema. Ejemplos: prolog, parlog.
    • Basado en restricciones: consiste en definir una serie de restricciones (y una función objetivo a optimizar), y un motor de búsqueda proporciona el conjunto de soluciones posibles (o la solución óptima). Ejemplos: GAMS, SQL.
  • Tipos de datos: los tipos de datos son una parte muy importante de un lenguaje, ya que determinarán en gran medida cómo es de propenso a inducir errores de programación, la flexibilidad a la hora de programar...
    • Tipado estático vs dinámico: es la principal característica que suele diferenciar a los lenguajes de script (aparte del intérprete, claro) de los lenguajes compilados. El tipado estático consiste en que los tipos de las variables han de ser declarados (o inferidos) y no pueden variar a lo largo de la ejecución del programa. El tipado dinámico da más flexibilidad y puede ahorrar algo de tiempo, al tener que escribir menos código, pero también es más propenso a que surjan errores (¡quiero un entero, no un string!).
      • Tipado fuerte vs débil: es otro matiz que puede tener el tipado dinámico. Con el tipado fuerte, las variables tienen un tipo concreto, mientras que con el tipado débil el tipo de las variables está indefinido y las operaciones que se pueden realizar con ellas depende de su valor, por ejemplo, con tipado fuerte "2" + "2" suele dar "22" y 2+2 da 4, mientras que con el tipado débil "2" y 2 son lo mismo, por lo que "2" + "2" dará "4".
      • Tipos paramétricos: algunos lenguajes de tipado fuerte, para facilitar la tarea de reutilización de librerías (normalmente de colecciones, como listas, árboles, grafos, etc.) permiten escribir código genérico que se puede especializar para cualquier tipo de dato.
    • Tipos nativos del lenguaje (built-in): además de los tipos más comunes (entero, booleano, coma flotante, string, array, puntero), algunos lenguajes tienen algunos tipos más complejos(tuplas, listas, tablas hash, matrices, etc.) incluidos en el lenguaje, mientras que otros los incorporan mediante librerías. Tener tipos complejos nativos es un arma de doble filo: por una parte puede ahorrar mucho tiempo y permitir escribir código más elegante, pero por otra parte se puede caer en un problema que yo llamo "pérdida de semántica" (aunque esa expresión se suele usar en el diseño de bases de datos). La pérdida de semántica consiste en que cuando se emplean de forma abusiva listas, tuplas ó matrices para representar la información, el código se hace ilegible, ya que se llena de accesos a atributos por índices (o peor aún, con cosas como cdddar). El código en Lisp es bastante propenso a sufrir este mal, mientras que en Matlab resulta inevitable. En Python la situación está equilibrada (depende mucho del programador) y en otros como Pascal es casi imposible caer.
  • Sintaxis:
    • Formato libre vs fijo: tiene que ver con el significado de los caracteres separadores (espacios en blanco, tabuladores), sobre todo al comienzo de las líneas. El formato fijo tiene mala fama por lenguajes como FORTRAN, que imponen restricciones absurdas (heredadas de la época de las tarjetas perforadas) en el formato del código, aunque otros lenguajes de formato fijo como Python son todo un referente en cuanto a elegancia y claridad de su código. Normalmente que el código sea de formato libre no es una ventaja, ya que un mal formateo puede inducir a malinterpretar el código.
    • Tamaño del lenguaje: generalmente es siempre una ventaja que un lenguaje sea sencillo, ya que facilita su aprendizaje, la comprensión del código escrito por otros programadores e incluso puede ayudar si se quiere realizar metaprogramación, es decir, generar código automáticamente con otros programas. Un lenguaje sencillo normalmente también es más fácil de analizar y por lo tanto se compilará/interpretará más rápidamente.
    • Ambigüedades: es una característica deseable en un lenguaje que todas las construcciones válidas tengan una única interpretación posible. Por desgracia, esto no siempre es así, por ejemplo este código válido en C: "int i = 0; i += i++ + i". Ni puta idea de lo que va a hacer, dependerá del compilador.
  • Introspección: consiste en poder obtener información sobre el programa mientras se está ejecutando. Normalmente permite obtener descripciones de las interfaces y llamar a funciones por su nombre. Se suele usar para cargar módulos dinámicamente, depurar, etc. Ejemplos: Java, Python.
  • Reflexión: es un paso más allá de la mera introspección, que consiste en modificar el propio programa mientras se está ejecutando. Algunos lenguajes van más allá de la simple modificación de código y permiten modificar la sintaxis y el compilador/intérprete. Aunque es una característica interesante y muy útil (sobre todo en los concursos de código ofuscado xD) que proporciona una tremenda potencia, un mal uso puede hacer un programa ilegible. Ejemplos: Lisp, Pike.
  • Implementación del lenguaje: estas son características que van más allá de lo que es el lenguaje en sí, refiriéndose a lo que realmente existe sobre ese lenguaje más allá de su gramática y semántica. Cobra mucho peso, porque al final estaremos limitados a lo que nos proporcionen las implementaciones de los lenguajes.
    • Estándar vs dispersión: ¿está el lenguaje normalizado por algún comité de estandarización, por una empresa o por un gurú? cada uno de estos aspectos tiene sus ventajas e inconvenientes. Los comités de estandarización tienen la ventaja de que los estándares suelen ser abiertos y garantizan compatibilidad entre distintas implementaciones, pero normalmente estancan la evolución del lenguaje. Las empresas pueden tener un modelo más o menos abierto y suelen impulsar cambios bastante rápidos, pero como tales siempre velarán por su propio interés antes que el de la comunidad de desarrolladores. Los gurús de los lenguajes (p.e. Guido van Rossum, Yukihiro Matsumoto) normalmente le imprimen un punto de vista personal al desarrollo del lenguaje y hacen de mediadores en los conflictos del resto de los desarrolladores del lenguaje, así que normalmente depende de quién esté al cargo que la cosa vaya bien o mal.
    • Documentación: el lenguaje más maravilloso del mundo si no está documentado no sirve para nada. Creo que eso lo dice todo.
    • Portabilidad: es deseable que un programa que has escrito en un ordenador pueda funcionar sin ningún cambio en otro, que la vida da muchas vueltas y nunca se sabe dónde va a acabar uno.
    • Rendimiento: el rendimiento es algo importante, pero hasta cierto punto. Si se trata de programas que se van a usar según salen del horno, hay que tener en cuenta que al tiempo de ejecución deberíamos sumarle el tiempo de compilación. Si son programas que duran poco el rendimiento es completamente obviable (un ser humano no va a distinguir si el programa se ejecuta en 1 o en 20 milisegundos). Un factor algo más importante es el consumo de memoria, ya que es un recurso que se suele agotar bastante más rápido que el tiempo (bueno, el tiempo es infinito, digamos la paciencia del usuario) y un programa que use mucha memoria probablemente tendrá penalizaciones de rendimiento por la no-localidad de los accesos (se le saca poco partido a las memorias caché) y si se da el caso de necesitar paginar memoria virtual se puede incrementar en órdenes de magnitud el tiempo de ejecución del programa. Por lo tanto, el rendimiento es un factor a considerar seriamente en videojuegos y cálculo científico.
    • Librerías comunes (estándar): otro factor realmente importante es la cantidad de código que nos podemos ahorrar porque ya lo ha escrito otro para nosotros. Si encima esas librerías vienen siempre con la distribución del lenguaje, nos ahorraremos tiempo buscándolas y espacio en la distribución del programa.
    • Entornos de desarrollo: he reservado para el final una característica que para mí es clave, pese a que normalmente se infravalora bastante. Un buen entorno de desarrollo puede convertir la tarea de programar en un placer. Nos puede facilitar la vida completando el código, mostrándonos la documentación sin tener que ir a buscarla, avisarnos de los errores antes de que probemos el programa, hacer cambios globales al programa (refactorización), permitiéndonos navegar rápidamente por todo nuestro código, con un entorno de depuración integrado, con editores visuales para interfaces gráficas... en definitiva, sin un buen IDE, un lenguaje no vale demasiado. Es algo que mucha gente olvida cuando se hacen comparativas de lenguajes y dos de los motivos por los que me encanta Java: Eclipse y NetBeans.
  • Otros factores: por último, algunas cosas que tienen que ver con la gente que usa los lenguajes:
    • Tamaño de la comunidad de usuarios: se supone que cuantas más personas haya usando un lenguaje menos probabilidad tendrá de morir.
    • Calidad de la comunidad de usuarios: desde luego que está bien que haya mucha gente, pero es preferible que haya poca gente pero dispuesta a ayudarse (compartir código, soluciones a problemas comunes) y que no genere demasiada basura alrededor del lenguaje.
    • Ofertas de trabajo: ¿te puede conseguir un buen curro conocer un lenguaje determinado? normalmente no es así, pero si cae, pues bienvenido sea... se oye mucho hablar de The Python Paradox (busca en google ;-) ).
Como la intención de este artículo no era comparar lenguajes, sino recopilar una serie de factores importantes a la hora de evaluarlos, aquí enlazo algunas comparativas, para que cada uno juzgue como quiera:
The Computer Language Shootout Benchmarks
Object Oriented Languages Comparison
Language Options Comparsion
99 Bottles of Beer: 1 program in 924* variations (* ese número va creciendo).

12 comentarios:

  1. Me has dejado con la boca abierta... :-O

    Muy bueno... sobre todo lo de "mi mierda huele mejor que la tuya"... tremenda frase que pasará a los anales de la programación XD

    ResponderEliminar
  2. Otra perspectiva:
    TCPI: Ranking TIOBE de lenguajes de programación. En Marzo de 2006, JAVA vuelve a ser una vez más el lenguaje de programación más popular, incrementando su diferencia respecto al resto.

    ResponderEliminar
  3. Hablando de mierda, seguro que pasa a los anales, no podr*a ser de otra forma xD.

    Cuando funcionan las cosas en un lenguaje declarativo da gusto; realmente no son tan complicados como parecen al principio. Exigen cambiar la forma de pensar, pero molan.

    Lo de los generics es elegante y punto. Cierto es que que resulta mas elegante usarlos que definirlos, pero bueno.

    Sobre el tipado debil... esa mierda no era buena hermano y mas aun cuando un lenguaje te permite usar variables tipadas y no tipadas a la vez. Hay mucho enfermo suelto. De hecho, una de las cosas que no acabo de ver en Java5 es la equivalencia entre tipos simples y objetos (int <-> Integer, long <-> Long y cosas de esas). No se puede decir que confundan porque son cosas muy simples, pero espero que se quede ahi la cosa.

    ResponderEliminar
  4. hombre, lo de usar variables tipadas y no tipadas a la vez tiene su encanto... siempre que por norma las uses tipadas y "en casos excepcionales" pues las uses dinámicas.

    ejemplo típico: he hecho un widget (pongamos que, en Java, heredando de JLabel) para mostrar el valor de algún otro widget (por ejemplo, un JSlider).
    Así que tengo una clasecilla más o menos así:

    public class MyWidget extends JLabel implements ChangeListener {
    private JSlider slider;
    public MyWidget(slider) {
    this.slider = slider;
    slider.addChangeListener(this);
    }

    public void onChange(ChangeEvent e) {
    super.setText(String.valueof(slider.getValue()));
    }
    }

    (no sé si el código compila, lo acabo de escribir a ojo).

    pues bien, pongamos que quiero reutilizar ese MyWidget para otro componente que tenga una interfaz compatible (getValue y addChangeListener), como puede ser un JSpinner...

    en este caso, supongo que es culpa de la jerarquía de clases de Java, que por aquí está algo mal hecha (estos elementos parece que no son muy MVC, como pueden ser el JComboBox y el JList, cuyos modelos son intercambiables), y realmente podría definirme yo mismo un interface con esos métodos y hacer un par de clases que extendiesen de JSlider y JSpinnBox (incluso podría adaptar más widgets a esa interfaz, qué gran patrón el Adapter), pero es una movida bastante gorda para la chorrada que realmente estoy haciendo.

    Otro caso donde se echa de menos el tipado dinámico es cuando haces un método que necesita como parámetro un objeto que implementa dos interfaces... aquí los genéricos vienen al rescate (aunque con una sintaxis bastante chunga, de la que no me acuerdo, pero que voy a buscar para quedar de puta madre)... pues no, pensaba que había algo del estilo

    public void foo(T v <T extends Interface1 && T extends Interface2>);

    Pero no he encontrado nada por ahí. Definitivamente, esta mierda no es nada buena xD

    Creo que en Objective-C están los "protocolos", que sirven concretamente para eso (y también los tipos dinámicos, claro!). Creo que ahí puede estar parte de la clave del éxito de los SDKs para Mac...

    Je, este comentario dara para otro post entero :p

    ResponderEliminar
  5. Supongo que estareis en pelotas mientras hablais de estas cosas, *verdad?

    ResponderEliminar
  6. Coño... oir mentar a alguien el Haskell me hace sentirme menos extraño. Hace tiempo leí algo sobre él, me hizo gracia y tengo por aquí unos cuantos manuales. Bellz se ríe mucho y Chuchi directamente me dice que estoy como una regadera.

    Con respecto a Java, comparto tu motivo principal. Eclipe es una puta maravilla y el Matisse de NetBeans....... sin palabras :)

    ResponderEliminar
  7. en la facultad de informática de la UPM enseñan programación funcional con Haskell... así que no es tan poco común ;-)

    ResponderEliminar
  8. En la facultad de informatica de la UPM seguro que se ponen en pelotas para hablar de Haskell

    ResponderEliminar
  9. es lo que tiene... nosotros además de ponernos en pelota hacemos el pino cuando hablamos de estas cosas xD

    ResponderEliminar
  10. Jajajja, excelente post :D.
    Por cierto: i += i++ + i; me dio 4 inicializando i en 1 :D. Bastante lógico si se ponen a pensar un momento ... bueno, se puede llegar a ese resultado considerando que i vale 1, entonces i++ devuelve 1, más el otro i que vale 1 da 2, pero como era un i++ ahora i vale 2, entonces el i += 2 da 4 :D.
    Ah, y el mejor lenguaje es Fortran... yo trabajo en Fortran y bueno, será más costumbre que otra cosa, pero hasta parsers de texto he hecho con éste, jeje (por cierto, eran para analizar código en fortran! jajaja). Ahí sí puedes tener reales ambigüedades cuando no trabajas con interfaces o sin módulos a lo 77 :).

    Saludos,

    FJRA

    ResponderEliminar
  11. depende de cómo se evalúe el operador +=

    si se evalúa primero la parte izquierda {i = i + (i++ +i)} debería dar 4, pero si se evalúa primero la parte derecha {i = (i++ +i) + i} daría 5 :p

    bueno, fortran es un lenguaje como otro cualquiera, lo único es que tiene cierta fama para el cálculo científico porque hay mucho código escrito... pero no tiene nada de especial.

    ResponderEliminar
  12. Sí pues, en realidad no se puede nunca hablar de un mejor lenguaje. Y eso que es un lenguaje cualquier, tampoco tampoco, más respeto... :D.

    Sí pues, esas librerías son las que muchas veces necesito :P.

    Ah, y cuando probé con i = 0 al inicio, me daba 1 el resultado... pero tienes razón, es cuestión del compilador...

    Saludos,

    ResponderEliminar