Los lenguajes de programación: tipología y evolución Els llenguatges de programació: tipologia i evolució Índice de contenido Els llenguatges de programació: tipologia i evolució...................................................................................1 Licencia......................................................................................................................................................1 Concepto y evolución histórica..................................................................................................................1 Conceptos básicos..................................................................................................................................1 Especificación de un lenguaje................................................................................................................1 Compilación, interpretación y depuración..............................................................................................3 Tipologías...................................................................................................................................................4 Imperativos.............................................................................................................................................4 Estructurados.....................................................................................................................................4 Orientados a objetos..........................................................................................................................4 Orientados a aspectos........................................................................................................................5 Declarativos............................................................................................................................................5 Funcionales.......................................................................................................................................6 Lógicos..............................................................................................................................................6 Evolución histórica....................................................................................................................................7 Primera generación: código máquina.....................................................................................................7 Segunda generación: Ensamblador.........................................................................................................7 Tercera generación: Lenguajes de alto nivel..........................................................................................7 Cuarta generación: Lenguajes declarativos no procedurales..................................................................8 Futuro: Programación por especificaciones, programación visual, lenguaje natural..............................8 Licencia Este obra de Jesús Jiménez Herranz está bajo una licencia Creative Commons AtribuciónLicenciarIgual 3.0 España. Basada en una obra en oposcaib.wikispaces.com. Concepto y evolución histórica Conceptos básicos Un lenguaje de programación es un conjunto de comandos y construcciones sintácticas que permiten representar un algoritmo en un ordenador. La idea es que sirvan como interfaz entre el lenguaje humano (fácil pero poco riguroso) y el lenguaje máquina (preciso pero difícil). Se dice que un lenguaje es universal o Turing-completo si tiene un poder computacional equivalente a una máquina de Turing, es decir, si cualquier algoritmo implementable por una máquina de Turing puede ser expresado en términos de ese lenguaje. La gran mayoría de lenguajes de programación son Turing-completos, existiendo excepciones como las fórmulas matemáticas de una hoja de cálculo, o las expresiones regulares. Gran parte de los lenguajes de programación se basan en los mismos conceptos básicos definidos por la arquitectura von Neumann y la máquina de Turing: la transformación de datos en una serie de pasos secuenciales. Ahora bien, existen muchos otros que se alejan de este paradigma, existiendo así diferentes tipologías de lenguajes. Especificación de un lenguaje Un lenguaje se define formalmente mediante gramáticas libres de contexto. Una gramática libre de contexto (GLC) es una tupla G(T,N,R,I), donde: Jesús Jiménez Herranz, http://oposcaib.wikispaces.com 1 Los lenguajes de programación: tipología y evolución ● ● ● ● T es un conjunto de elementos terminales N es un conjunto de elementos no terminales R es un conjunto de reglas de transformación, en el que cada regla r es una función r : T P T ∪N I es el símbolo no terminal inicial de la gramática, I ∈ N Una gramática G define un lenguaje, compuesto por todas aquellas combinaciones de elementos terminales que pueden ser producidos mediante la aplicación de un conjunto de reglas al símbolo inicial de la gramática. Generalmente una gramática se especifica mediante sus reglas, quedando implícitos los terminales y no terminales en las propias reglas, y el elemento inicial I como el que aparece en la primera regla. Por ejemplo, una gramática que defina el lenguaje de las expresiones aritméticas sobre números enteros podría ser la siguiente: Expr: Expr Oper Expr | (Expr) | Num Oper: + | - | * | / Num: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 En este caso, los símbolos terminales serían los dígitos del 0 al 9, los operadores matemáticos y los paréntesis, mientras que los símbolos no terminales serían Expr, Oper y Num. Para hacer más compacta la notación, se unen las reglas con el mismo símbolo origen, separando el resultado por barras verticales. Para saber si una expresión pertenece al lenguaje de esta gramática, hay que encontrar una secuencia de reglas que lleve a ella desde el símbolo inicial. Por ejemplo, para la cadena “5*(2+3)” podríamos encontrar la secuencia: Expr -► Expr Oper Expr -► Expr * Expr -► Num * Expr -► 5*Expr -► 5*(Expr) -► 5*(Expr Oper Expr) -► 5*(Expr+Expr) -► 5*(Expr+Num) -► 5*(Num+Num) -► 5*(2+Num) -► 5*(2+3) En el caso de los lenguajes de programación, se acostumbra a utilizar la notación BNF, propuesta por Backus y Naur (Backus-Naur Form). Por ejemplo, la siguiente gramática BNF especificaría una versión simplificada del Pascal: <PROGRAMA> <MAIN> <FUNCIONES> ::= ::= ::= <FUNCION> <PARAMETROS> ::= ::= <LINEAS> ::= <LINEA> ::= <IDENTIFICADOR>::= <EXPRESION> ::= <OPERACION> ::= program <FUNCIONES> <MAIN> begin <LINEAS> end. <FUNCION> <FUNCIONES> | <FUNCION> [procedure|function] (<PARAMETROS>) begin <LINEAS> end; <IDENTIFICADOR>,<PARAMETROS> | <IDENTIFICADOR> <LINEA>; <LINEAS> | <LINEA> <IDENTIFICADOR> := <EXPRESION> [a-zA-Z0-9] <IDENTIFICADOR> | [a-zA-Z0-9] <IDENTIFICADOR> | <EXPRESION> <OPERACION> <EXPRESION> | <IDENTIFICADOR>(<PARAMETROS>) + | - | * | / Jesús Jiménez Herranz, http://oposcaib.wikispaces.com 2 Los lenguajes de programación: tipología y evolución Esta gramática reconocería programas como este: program function promedio(a,b) begin temp:=a+b; promedio:=temp/2; end; begin n1:=5; n2:=13; resultado:=promedio(n1,n2); end. Compilación, interpretación y depuración El hecho de utilizar un lenguaje de programación hace necesario un procesado posterior que traduzca este lenguaje al código máquina inteligible por el ordenador. Hay básicamente dos formas de llevar a cabo este proceso: ● ● Compilación: El programa se transforma en su equivalente en código máquina, y se almacena así para el momento de la ejecución. Interpretación: El programa se almacena como código fuente, y en el momento de la ejecución se va traduciendo a código máquina conforme es necesario. Cada uno de estos esquemas tiene sus ventajas e inconvenientes. Las ventajas de la compilación serían: ● Código más rápido y eficiente: Como sólo se hace una vez, se puede invertir un tiempo en optimizar a fondo el programa resultante. Por el contrario, los lenguajes interpretados tienen otras ventajas respecto a los compilados: ● Mayor portabilidad: Al almacenarse los programas en código de alto nivel, para llevar el programa de una arquitectura a otra basta con que exista un intérprete. Acceso al código: Es posible acceder y modificar el código de un programa interpretado en cualquier momento. Posibilidad de optimizar para la máquina destino: En el caso de familias de ordenadores, es posible optimizar de forma más eficiente al conocer los detalles del ordenador en que se está ejecutando. En la compilación, se optimiza para el mínimo común denominador, lo que no siempre aprovecha las características avanzadas de las máquinas más modernas. ● ● Si bien los lenguajes interpretados tienen muchas ventajas, su bajo rendimiento ha lastrado su uso en determinados entornos en los que es deseable una buena velocidad de ejecución. De todas formas, existen medidas que alivian estas diferencias. Por ejemplo, en vez de ir interpretando el código a medida que se ejecuta, los intérpretes JIT (Just In Time) hacen una compilación la primera vez que se arranca el programa, guardándola para ejecuciones sucesivas. Esto, añadido a la posibilidad de hacer optimizaciones más específicas para la máquina concreta que en el caso de la compilación previa, hace que las diferencias de rendimiento entre lenguajes compilados e interpretados se hayan reducido, e incluso que en algunas ocasiones resulte más eficiente utilizar un programa interpretado. Esta convergencia en el rendimiento, sumada a las características de portabilidad y versatilidad de los lenguajes interpretados han hecho que éstos se extiendan en gran manera en ámbitos como el de los servidores o el desarrollo de aplicaciones web. En realidad, tanto los lenguajes más utilizados Jesús Jiménez Herranz, http://oposcaib.wikispaces.com 3 Los lenguajes de programación: tipología y evolución actualmente en estos entornos (Java), como los nuevos desarrollos en lenguajes (Python, Ruby) son interpretados. Tipologías Imperativos Los lenguajes imperativos son los más cercanos al modelo de la máquina de Turing, en el sentido de que expresan un programa como un conjunto secuencial de acciones que actúan sobre los datos y el estado de la máquina. Concretamente, un lenguaje imperativo dispone de los siguientes elementos: ● ● ● Variables en las que guardar valores temporales Operaciones para operar sobre las variables Instrucciones de salto para alterar el flujo de ejecución Todos los lenguajes de ensamblador son imperativos por motivos obvios, así como muchos lenguajes de alto nivel. La programación imperativa se subdivide en otros paradigmas, como la programación estructurada o la programación orientada a objetos. Estructurados El paradigma estructurado es un refinamiento del imperativo. Así, un programa estructurado se compone de los siguientes elementos: ● ● ● Bloques de sentencias que se ejecutan secuencialmente (procedimientos o funciones). Estructuras de selección del bloque a ejecutar a partir de una condición (condicionales). Estructuras de repetición de bloques de código según una condición (bucles). En concreto, el paradigma estructurado elimina el concepto de salto, que queda implícito en los bucles, las construcciones condicionales y la división del código en procedimientos. El paradigma estructurado tiene una serie de ventajas: ● ● Mayor legibilidad del código, lo que facilita la programación y el mantenimiento. Encapsulamiento de la funcionalidad, lo que facilita las modificaciones futuras y el testeo. Ejemplos de lenguajes estructurados serían el C o el Pascal. Orientados a objetos La orientación a objetos supone un cambio importante respecto al paradigma estructurado. La idea subyacente es que la separación entre los datos y las operaciones que los manipulan que establece el modelo estructurado es artificial, ya que en el mundo real son aspectos que van de la mano. Por otra parte, en el modelo estructurado no hay diferencias claras entre las diferentes entidades que son relevantes a un programa, cuando en realidad los problemas a resolver consisten generalmente en interacciones entre diferentes entidades. Así, el paradigma de la orientación a objetos propone plantear los problemas en términos de unas ciertas entidades, llamadas objetos, y sus interacciones. Cada uno de estos objetos estará compuesto de unos datos, así como de ciertas operaciones sobre los mismos. La orientación a objetos se basa en tres puntos fundamentales: Jesús Jiménez Herranz, http://oposcaib.wikispaces.com 4 Los lenguajes de programación: tipología y evolución ● ● ● Encapsulación: Los programas están formados por objetos, con sus datos y operaciones sobre ellos. Además, los detalles de cada objeto generalmente no son relevantes para otros objetos, por lo que sólo son accesibles por el objeto propietario. Herencia: Los objetos se constituyen como una jerarquía de clases, de manera que una clase puede heredar características de otra, ampliando o modificando su funcionalidad. Esto concuerda con numerosas problemáticas del mundo real, en la que hay evidentes patrones de herencia. Polimorfismo: Capacidad de que objetos de diferentes tipos respondan a métodos que, aunque tienen el mismo nombre, actúan de forma diferente en cada tipo. Esto permite abstraer las diferencias de implementación de las operaciones para cada tipo, y escribir programas a muy alto nivel. Por ejemplo, el operador “+” puede usarse tanto para sumar enteros como para concatenar cadenas. Java o C++ serían ejemplos de lenguajes orientados a objetos. Orientados a aspectos Este paradigma de programación es una evolución de la programación orientada a objetos. La idea subyacente es que gran parte del código del programa, pese a tener una temática común, no se puede encapsular convenientemente y afecta de manera horizontal a toda la aplicación. Por ejemplo, en una aplicación de gestión probablemente haya que gestionar la conexión a las bases de datos, la autenticación, o el logging de sucesos. Por tanto, probablemente en cada método de la lógica de negocio sea necesaria algún tipo de intervención referente a estos aspectos, volviéndose el código menos legible y dificultando su mantenimiento en el caso de que alguno de estos aspectos cambie en el futuro. Aun encapsulando estas gestiones en clases independientes, sería necesario añadir en cada método del programa las llamadas correspondientes, por lo que no se resuelve el problema. Lo que propone la POA es encapsular estas funcionalidades comunes en entidades llamadas aspectos. Cada aspecto, además de la implementación de su funcionalidad, indicaría su relación con el resto de elementos del programa. Así, en el ejemplo anterior, se crearían los aspectos Seguridad, BD y Logging, estableciendo que los diferentes métodos de la lógica de negocio hacen referencia a ellos. Por tanto, el proceso con POA es: ● ● ● Identificar clases y aspectos Implementar por separado Enlazar aspectos a las clases que los necesiten (weaving) Ventajas de la POA: ● ● El código es más legible Es posible añadir funcionalidad sin tocar el código principal, sólo añadiendo un aspecto Para implementar POA se puede usar desde preprocesadores hasta lenguajes específicos, pasando por extensiones de lenguajes existentes. Declarativos Los lenguajes declarativos, en oposición a los imperativos, no indican una serie de pasos a seguir, sino que indican el resultado que se desea obtener. La implementación concreta necesaria para llegar a ese resultado es algo interno al compilador. Jesús Jiménez Herranz, http://oposcaib.wikispaces.com 5 Los lenguajes de programación: tipología y evolución Al no indicar los detalles de implementación del algoritmo, sino únicamente el resultado, los lenguajes declarativos dejan mucho más margen al compilador para la optimización, por lo que se utilizan comúnmente para programas de alta concurrencia, difíciles de programar directamente. Un ejemplos típico de lenguaje declarativo serían las consultas de SQL, en las que se indica el resultado a obtener, dejando al SGBD los detalles sobre cómo recorrer las tablas, qué índices utilizar, etc. Funcionales La programación funcional concibe la ejecución de un programa como la evaluación de diferentes funciones matemáticas. En este caso, no existe el concepto de una serie secuencial de sentencias, ni de una máquina central con estado, por lo que este paradigma es completamente independiente del modelo de la máquina de Turing. Algunas características de estos lenguajes son: ● ● ● ● ● Todo son funciones, que devuelven datos y/o otras funciones Muy paralelizables si las funciones son puras (reentrantes) La gestión de memoria está implícita Recursividad en vez de iteratividad (que es un caso particular de recursividad) Fácil probar matemáticamente corrección Tradicionalmente, los lenguajes funcionales han sido más lentos que los imperativos, si bien implementan aspectos como la gestión de memoria y construcciones recursivas complejas que son ignorados por los lenguajes imperativos. Conforme los lenguajes imperativos han ido implementando características como la recolección automática de basura, o técnicas avanzadas de orientación a objetos (como la reflexión), la diferencia se ha reducido, y a día de hoy no es relevante. De hecho, en ámbitos científicos en los que se requiere una gran capacidad de cálculo, es común ver lenguajes típicamente imperativos como Fortran sustituidos por otros funcionales, que permiten expresar los cálculos de una forma más limpia y compacta y ofrecen unas capacidades de paralelización que los hacen especialmente apropiados para la tarea. La que posiblemente sea la principal dificultad de los lenguajes funcionales es el aprendizaje de un nuevo paradigma completamente diferente al tradicional de la máquina de Turing. Debido a esto, los lenguajes funcionales tienen un ámbito de aplicación reducido, básicamente, a entornos académicos y aplicaciones científicas y de inteligencia artificial. Algunos lenguajes funcionales son Lisp y sus dialectos (si bien dispone de muchas extensiones que hacen que no sea puramente funcional), o Haskell. Muchos lenguajes imperativos modernos como Python o Ruby están también claramente influenciados por los lenguajes funcionales, y adoptan algunas de sus construcciones, por lo que se pueden usar de forma más o menos funcional. El carácter abstracto y altamente paralelo de los lenguajes funcionales puros los hacen especialmente apropiados para la computación cuántica, y de hecho ya existen dialectos de Haskell y otros lenguajes funcionales en este sentido. Lógicos En un lenguaje lógico, un programa se compone de una base de conocimiento en forma de predicados de lógica de primer orden. El funcionamiento del programa consistiría en hacer preguntas a esta base de conocimiento, cuya respuesta sería deducida de estos predicados. Por ejemplo, si consideramos la siguiente base de conocimiento: Jesús Jiménez Herranz, http://oposcaib.wikispaces.com 6 Los lenguajes de programación: tipología y evolución ave x⇒ vuela x ave paloma aveaguila ¬ave perro podríamos preguntar cosas como: vuela paloma? verdadero vuela? paloma , aguila Las aplicaciones principales de los lenguajes lógicos están en el ámbito de la inteligencia artificial y los sistemas expertos, si bien son directamente aplicables a determinados problemas comunes (p. ej. cálculo de horarios) que son difíciles de resolver por métodos imperativos. Por otra parte, los programas lógicos requieren un trabajo importante de construcción de la base de conocimientos, son difíciles (si no imposibles) de depurar, y además el tiempo de ejecución puede dispararse con facilidad. El principal lenguaje lógico es el Prolog. Evolución histórica Primera generación: código máquina La primera generación de lenguajes de programación consistía en escribir programas en código máquina, es decir, introduciendo directamente en el ordenador los valores binarios correspondientes a los códigos de instrucción, datos, etc., generalmente mediante interruptores y palancas. Cronológicamente, este esquema fue el que se uso en los inicios de la informática, en los años 40 y 50. Segunda generación: Ensamblador La segunda generación de lenguajes se corresponde con la aparición del lenguaje ensamblador. El ensamblador no era más que una representación textual del código máquina que se usaba hasta entonces, de manera que a cada instrucción se le asignase un nombre identificativo, y los datos pudieran ser introducidos en decimal/hexadecimal en vez de directamente en binario. Un programa ensamblador se encargaba de traducir el texto del código ensamblador a formato binario inteligible por el ordenador. Tercera generación: Lenguajes de alto nivel La tercera generación incluye a la mayoría de lenguajes modernos, y se refiere a la aparición de lenguajes de alto nivel. Estos lenguajes ofrecen estructuras sintácticas complejas, que permiten implementar fácilmente funcionalidades como bucles o condicionales, difíciles de implementar directamente con el juego de instrucciones de un ordenador. A partir del programa, un programa compilador se encarga de traducir el código de alto nivel a su equivalente en ensamblador y/o código máquina. Uno de los primeros lenguajes de alto nivel fue Fortran, creado a finales de los años 50. Dentro de esta generación se encuentran tanto los primeros lenguajes imperativos como Fortran o Cobol, como lenguajes lógicos y funcionales como Prolog o Lisp. También se incluyen aquí Jesús Jiménez Herranz, http://oposcaib.wikispaces.com 7 Los lenguajes de programación: tipología y evolución lenguajes estructurados como Pascal o C, o lenguajes orientados a objetos como Smalltalk, C++ o Java. Cuarta generación: Lenguajes declarativos no procedurales Si bien a partir de la tercera generación la terminología es más difusa, generalmente se entiende como lenguajes de cuarta generación a los lenguajes declarativos, que a diferencia de los imperativos especifican el objetivo a calcular (objetivo explícito, procedimiento implícito) en lugar de los pasos concretos a seguir (objetivo implícito, procedimiento explícito). En esta categoría se encontrarían lenguajes como SQL, Mathematica o Postscript. Generalmente, los lenguajes de 4GL, aunque en muchos casos son Turing-completos, están orientados a un dominio específico, como es el caso de las bases de datos en SQL o la impresión de documentos en Postscript. Futuro: Programación por especificaciones, programación visual, lenguaje natural Hay quien habla de una quinta generación de lenguajes de programación, en la que, por ejemplo, se programaría mediante especificaciones de cómo debe ser el programa, sin indicar los detalles de implementación. Lenguajes como Prolog o SQL apuntan hacia esta dirección. Otra propuesta de lenguaje de quinta generación sería un lenguaje completamente visual, en el que los diferentes componentes serían bloques que podrían ensamblarse unos con otros de forma totalmente gráfica, y sin entrar en detalles de implementación. En este sentido se ha comparado el desarrollo de software con otros ámbitos de la ingeniería, como la construcción o el diseño de maquinaria, en los que en lugar de hacer cada diseño desde cero, se utilizan una serie de componentes comunes para llevar a cabo diseños más complejos. Finalmente, el objetivo final de la programación sería poder especificar al ordenador cómo debe ser un programa directamente usando el lenguaje natural. Esto entra en el ámbito de la inteligencia artificial, por lo que ni siquiera está claro que sea un problema resoluble, ni por otra parte que sea la mejor forma de especificar un programa dadas la poca precisión y las ambigüedades intrínsecas a este tipo de lenguaje. Jesús Jiménez Herranz, http://oposcaib.wikispaces.com 8