Qué es un lenguaje de programación
Qué es un programa y en qué se diferencia de un algoritmo, por qué hay que traducirlo para que la máquina lo ejecute, los tres niveles de lenguaje, la diferencia entre compilar e interpretar, el caso intermedio de la máquina virtual y los grandes paradigmas.
Qué es un programa y qué es un algoritmo
Un ordenador no decide nada por su cuenta ni entiende lo que hace. Lo único que sabe es ejecutar órdenes muy simples, una detrás de otra, a una velocidad enorme: llevar un dato de un sitio a otro, sumar dos números, comparar si uno es mayor que otro y poco más. Un programa es exactamente eso: la lista de órdenes que alguien ha escrito para que la máquina resuelva un problema concreto.
Pero antes del programa hay algo previo, y conviene separarlo bien porque se pregunta. Antes de escribir nada hay que tener el plan: la serie de pasos que resuelven el problema. A ese plan se le llama algoritmo, y se puede pensar y escribir en una servilleta sin tocar un ordenador. Para calcular la nota media de los test que ha hecho un alumno, el algoritmo es «suma todas las notas, cuenta cuántas hay y divide lo primero entre lo segundo». El programa es ese mismo plan escrito con las palabras exactas y la puntuación exacta que exige un lenguaje de programación.
- Finito: tiene que terminar. Un procedimiento que no acaba nunca no es un algoritmo.
- Preciso: cada paso está definido sin ambigüedad. «Calcula la nota» no vale; «divide la suma entre el número de notas» sí.
- Determinista: con los mismos datos de entrada da siempre el mismo resultado.
- Tiene entrada y salida: parte de unos datos (las notas) y produce un resultado (la media).
El algoritmo es el plan y el programa es ese plan escrito en un idioma concreto. Un mismo algoritmo se puede programar en veinte lenguajes distintos y sigue siendo el mismo algoritmo.
Para el examen
Algoritmo: el plan de pasos: finito, preciso, determinista, con entrada y salida
Programa: ese plan escrito en un lenguaje concreto
Qué es un lenguaje de programación
Un lenguaje de programación es un idioma artificial, inventado a propósito para escribir programas. Se parece a un idioma humano en que tiene palabras y reglas para combinarlas, y se diferencia en dos cosas importantes: el vocabulario es minúsculo (unas pocas decenas de palabras reservadas, que no se pueden usar para otra cosa) y la gramática no admite ni una sola excepción. Donde una persona entiende de sobra una frase mal construida, un lenguaje de programación se para y da un error.
Todo lenguaje se describe en tres capas, y saber cuál es cuál basta para acertar la pregunta clásica sobre en qué se diferencia un error de sintaxis de uno semántico.
| Capa | Qué define | Ejemplo de fallo |
|---|---|---|
| Léxico | Qué palabras y símbolos existen en el lenguaje | Escribir «whille» donde el lenguaje solo conoce «while» |
| Sintaxis | Cómo se combinan esas palabras para formar instrucciones válidas | Abrir un paréntesis y no cerrarlo, u olvidar el punto y coma final |
| Semántica | Qué significa lo que está escrito, es decir, qué hace al ejecutarse | Dividir entre una variable que vale cero: está perfectamente escrito y aun así no tiene sentido |
Lo que escribe la persona se llama código fuente y es un archivo de texto normal y corriente, legible con cualquier editor. Lo que ejecuta el procesador es código máquina, que no es legible para nadie. Entre esos dos extremos está todo el trabajo del que se ocupan los compiladores y los intérpretes.
Un error de sintaxis es una falta de gramática y lo detecta el traductor antes de ejecutar nada. Un error semántico está bien escrito y solo se manifiesta al ejecutarse, o ni siquiera eso: simplemente el programa da un resultado equivocado.
Para el examen
Léxico: qué palabras existen
Sintaxis: cómo se combinan
Semántica: qué significan
Error sintáctico frente a semántico: el sintáctico lo caza el traductor; el semántico está bien escrito y falla al ejecutar
Los tres niveles: máquina, ensamblador y alto nivel
Los lenguajes se ordenan por su cercanía a la máquina o a la persona. En el nivel más bajo está el lenguaje máquina: instrucciones escritas en ceros y unos que el procesador ejecuta directamente, sin traducción de ningún tipo. Es el único idioma que un procesador entiende de verdad, y cada familia de procesadores tiene el suyo, distinto e incompatible con los demás. Hoy nadie programa así.
Un escalón por encima está el ensamblador. Es lenguaje de bajo nivel, pero ya no son ceros y unos: cada instrucción máquina recibe una abreviatura pronunciable, un mnemotécnico, del estilo de MOV para mover un dato o ADD para sumar. La correspondencia es prácticamente de una instrucción de ensamblador por cada instrucción máquina, y el programa que hace esa traducción se llama ensamblador también. Sigue atado al procesador concreto: un programa en ensamblador de una familia no funciona en otra.
Arriba están los lenguajes de alto nivel, que son con los que se trabaja hoy: C, Java, Python, C#, JavaScript. Se escriben con palabras del inglés y notación parecida a la matemática, no dependen del procesador y una sola línea suya se convierte en muchas instrucciones máquina. Esa es su ventaja principal: el mismo código fuente sirve para máquinas distintas, porque lo que cambia es el traductor, no el programa.
; LENGUAJE MAQUINA: lo unico que ejecuta el procesador
10110000 00100010
00000100 00010000
; ENSAMBLADOR: una abreviatura por cada instruccion maquina
MOV AL, 34 ; mete el 34 en un registro del procesador
ADD AL, 16 ; le suma 16 a lo que hubiera en ese registro
; ALTO NIVEL: una sola linea hace todo lo anterior
totalPreguntas = 34 + 16Las tres versiones del ejemplo hacen lo mismo: sumar 34 y 16. La primera es lo que acaba corriendo el procesador y es ilegible. La segunda dice lo mismo con dos órdenes nombradas, pero obliga a saber que existe un registro llamado AL y a decidir a mano dónde se guarda cada cosa. La tercera se lee sola y no menciona el hardware por ninguna parte: de dónde saca la máquina el sitio para guardar el resultado ya es problema del traductor.
A veces esta misma clasificación se expone como generaciones: primera generación el lenguaje máquina, segunda el ensamblador, tercera los lenguajes de alto nivel de propósito general y cuarta los de propósito específico y muy alto nivel, en los que se describe el resultado que se quiere en vez del procedimiento, como SQL para consultar bases de datos.
Cuanto más alto es el nivel, más cerca está el lenguaje de la persona, más portable es el programa y menos control directo se tiene sobre el hardware. Cuanto más bajo, justo al revés.
Para el examen
Lenguaje máquina: ceros y unos: lo único que ejecuta el procesador
Ensamblador: un mnemotécnico por instrucción máquina; atado al procesador
Alto nivel: portable; una línea equivale a muchas instrucciones máquina
Generaciones: de 1GL a 4GL
Traducir el programa: compilar, interpretar y el caso intermedio
Como el procesador solo entiende código máquina, el código fuente que escribe la persona no se puede ejecutar tal cual: hay que traducirlo. Y hay dos maneras de hacerlo, que es una de las distinciones que más se preguntan.
Un compilador lee el programa entero de una vez, lo revisa de arriba abajo y, si todo está bien, produce un archivo ejecutable con el código máquina equivalente. A partir de ahí el código fuente ya no hace falta para nada: se ejecuta el archivo generado, tantas veces como se quiera. Si hay un error de sintaxis en cualquier línea, el compilador no genera nada y el programa ni siquiera llega a arrancar.
Un intérprete trabaja al revés: no genera ningún archivo. Coge la primera instrucción, la traduce, la ejecuta, pasa a la siguiente y así hasta el final. Y lo repite entero cada vez que se lanza el programa. Eso lo hace más lento, porque la traducción se paga en cada ejecución, y hace que un error de sintaxis escondido en una línea a la que casi nunca se llega pueda tardar meses en aparecer.
COMPILADO (C, C++, Go, Rust)
codigo fuente --> [ COMPILADOR ] --> ejecutable --> [ PROCESADOR ]
una sola vez muchas veces
INTERPRETADO (Python, JavaScript, PHP, Ruby)
codigo fuente --> [ INTERPRETE lee y ejecuta linea a linea ]
en cada ejecucion, de principio a fin| Compilado | Interpretado | |
|---|---|---|
| Cuándo se traduce | Una vez, antes de ejecutar | En cada ejecución, sobre la marcha |
| Qué produce | Un archivo ejecutable independiente | Nada: no queda ningún archivo |
| Velocidad de ejecución | Mayor, porque ya está todo traducido | Menor, porque traduce mientras ejecuta |
| Cuándo aparecen los errores de sintaxis | Antes de ejecutar, todos de golpe | Al llegar la ejecución a esa línea |
| Portabilidad | Hay que compilar de nuevo para cada sistema | El mismo fuente corre donde haya intérprete |
| Hace falta en el ordenador del usuario | Solo el ejecutable | El código fuente y el intérprete instalado |
Entre esos dos extremos hay un camino intermedio, y se pregunta mucho porque no encaja del todo en ninguna de las dos casillas anteriores. Java y C# lo siguen: el código fuente se compila, sí, pero no a código máquina de ningún procesador real, sino a un código intermedio llamado bytecode, pensado para una máquina que no existe físicamente. Después, un programa llamado máquina virtual (la Máquina Virtual Java o JVM en el caso de Java, el CLR en el de .NET) va leyendo ese bytecode y lo ejecuta sobre la máquina real.
Notas.java --> [ COMPILADOR ] --> Notas.class (bytecode)
|
+-----------------+-----------------+
| | |
[ JVM Windows ] [ JVM Linux ] [ JVM macOS ]
| | |
se ejecuta se ejecuta se ejecuta
Se compila UNA vez. Lo que cambia en cada sistema es la maquina
virtual, no el programa.El resultado es que el mismo archivo compilado corre en cualquier sistema que tenga instalada su máquina virtual, idea que Java resumió como «escribe una vez, ejecuta en cualquier parte». El precio es esa capa intermedia, y para recuperar parte de la velocidad perdida las máquinas virtuales modernas usan compilación JIT (just in time, «justo a tiempo»): mientras el programa corre, detectan los trozos de bytecode que más se repiten y los compilan a código máquina nativo ahí mismo.
El compilador traduce todo antes y deja un ejecutable; el intérprete traduce y ejecuta a la vez y no deja nada; la máquina virtual está en medio. Java no es puramente compilado ni puramente interpretado, y su portabilidad no la da el lenguaje, la da tener una máquina virtual distinta para cada sistema.
Para el examen
Compilador: traduce todo antes y deja un ejecutable independiente
Intérprete: traduce y ejecuta línea a línea, en cada ejecución
Errores de sintaxis: todos de golpe antes de ejecutar, frente a al llegar a esa línea
Portabilidad: el compilado se recompila por sistema; el interpretado corre donde haya intérprete
Java y C#: compilan a bytecode que ejecuta una máquina virtual (JVM, CLR), con compilación JIT
Paradigmas de programación
Un paradigma no es un lenguaje: es una forma de plantear la solución, un estilo de pensar el programa. Un mismo lenguaje puede permitir varios paradigmas (Python o Java admiten al menos tres), y por eso la pregunta de examen casi nunca es «qué paradigma es Java», sino «en qué consiste tal paradigma».
En el paradigma imperativo el programa es una secuencia de órdenes que van cambiando el estado de la memoria: haz esto, luego esto otro, y si se cumple tal cosa haz aquello. Se describe el cómo, paso a paso. Su versión disciplinada es la programación estructurada, que prohíbe los saltos arbitrarios y construye cualquier programa con solo tres estructuras: secuencia, selección y repetición. Todo lo que se ve en los subtemas siguientes de este tema es programación imperativa estructurada.
En el paradigma orientado a objetos los datos y las operaciones que los manipulan no van por separado, sino empaquetados juntos en objetos. Un objeto «alumno» guarda su nombre y sus notas, y además sabe calcular su propia media. Sus mecanismos característicos son la encapsulación, la herencia y el polimorfismo. Java, C#, C++ y Python trabajan así.
En el paradigma funcional el programa se construye combinando funciones, como en matemáticas, y se evita modificar el estado: una función recibe unos datos y devuelve un resultado sin tocar nada de fuera. Lisp, Haskell y Scala son los ejemplos clásicos, aunque muchas de sus ideas se han incorporado a Java y a JavaScript.
En el paradigma declarativo se describe qué se quiere obtener y no cómo obtenerlo: el cómo lo decide el sistema. El caso más conocido es SQL, y también son declarativos HTML, que describe la estructura de una página sin decir cómo dibujarla, y la programación lógica de Prolog, basada en hechos y reglas.
-- DECLARATIVO: se describe el resultado que se quiere
SELECT nombre, AVG(nota)
FROM intentos
WHERE nota < 5
GROUP BY nombre;
-- En ningun sitio se dice como recorrer la tabla, en que orden,
-- ni donde ir acumulando las sumas. Eso lo decide el gestor.La frontera que se pregunta es imperativo frente a declarativo: el imperativo dice cómo hacerlo y el declarativo dice qué se quiere. Una consulta SQL no explica el recorrido de la tabla, describe el resultado.
Para el examen
Imperativo: describe el CÓMO, paso a paso
Orientado a objetos: datos y operaciones juntos
Funcional: combinar funciones, sin estado
Declarativo: describe el QUÉ; SQL es el ejemplo
Otros paradigmas: procedimental, lógico y reactivo
Los cuatro paradigmas del punto anterior son los que se preguntan siempre, pero las clasificaciones al uso manejan alguno más. Casi todos son subdivisiones o combinaciones de los cuatro grandes, y esa es justamente la clave para no perderse: no son ocho familias independientes.
El paradigma procedimental no se opone al imperativo: es una variedad suya. Los dos describen el cómo paso a paso, pero el imperativo a secas se conforma con una lista de órdenes, mientras que el procedimental exige agrupar esas órdenes en procedimientos con nombre a los que se llama desde donde haga falta. Es lo que resuelve el problema de un programa que crece hasta hacerse inmanejable en un solo bloque, y en la práctica es como se programa en C o en Pascal. Todo lo que se ve en este tema a partir del subtema de funciones es programación procedimental.
El paradigma lógico es una rama del declarativo. En lugar de escribir instrucciones, se declaran hechos que se dan por ciertos y reglas que relacionan unos hechos con otros, y después se hacen preguntas. El programa no ejecuta pasos: un motor de inferencia busca por su cuenta qué combinaciones de hechos y reglas responden a la pregunta. Su lenguaje característico es Prolog, y su terreno natural son los sistemas expertos y el razonamiento automático.
% HECHOS: lo que se da por cierto
superado(ana, tema1).
superado(ana, tema2).
requiere(tema3, tema1).
% REGLA: cuando se puede empezar un tema
puede_empezar(Alumno, Tema) :-
requiere(Tema, Previo),
superado(Alumno, Previo).
% PREGUNTA
?- puede_empezar(ana, tema3).
-> si
% En ningun sitio se ha escrito COMO buscarlo: ni un bucle, ni un if.
% Solo se ha declarado lo que es cierto y que significa poder empezar.El ejemplo declara que Ana ha superado los temas 1 y 2, y que el tema 3 exige haber superado el tema 1. La regla dice qué significa poder empezar un tema: que exista un tema previo requerido y que el alumno lo tenga superado. Al preguntar si Ana puede empezar el tema 3, el motor encuentra solo la combinación que encaja y responde que sí. No hay recorrido escrito por ninguna parte.
El paradigma reactivo se organiza alrededor de flujos de datos que cambian con el tiempo y de la propagación automática de esos cambios. En vez de calcular un valor y olvidarse, se declara que un valor depende de otros, y cuando alguno de ellos cambia, todo lo que dependía de él se recalcula solo. La analogía exacta es una hoja de cálculo: al escribir en una celda la fórmula de la suma de otras dos, no hay que volver a ejecutarla nunca; cambia un sumando y el total se actualiza al instante. Es el modelo de las interfaces modernas y del tratamiento de eventos continuos, donde el dato llega cuando quiere y el programa tiene que reaccionar.
Queda un término que aparece en algunas listas y conviene aclarar: el paradigma matemático. No es una familia aparte, sino otro nombre para el funcional en su versión pura, donde las funciones se comportan exactamente como en matemáticas (sin estado, sin efectos laterales y con el mismo resultado ante los mismos argumentos). Se cita aquí porque puede aparecer enunciado así, pero lo que describe ya está cubierto por el paradigma funcional.
Procedimental es una variedad del imperativo, y lógico una variedad del declarativo. El matemático no es un paradigma propio: es el funcional puro. Los realmente independientes siguen siendo los cuatro grandes.
Para el examen
Procedimental: imperativo con procedimientos con nombre: C, Pascal
Lógico: declarativo con hechos y reglas: Prolog
Reactivo: flujos que propagan cambios, como una hoja de cálculo
El llamado «matemático»: es el funcional puro
Por dentro de un compilador y el enlazado
Compilar no es una operación única: es una cadena de fases, y cada una recibe lo que produjo la anterior. Se suele dividir en una etapa de análisis, que entiende el programa de entrada, y una de síntesis, que construye el programa de salida.
- Análisis léxico: parte el texto en piezas mínimas llamadas tokens (una palabra reservada, un nombre de variable, un número, un símbolo). Aquí se cazan las palabras que no existen.
- Análisis sintáctico: comprueba que esas piezas están combinadas según la gramática y construye con ellas un árbol sintáctico. Aquí se cazan los paréntesis sin cerrar.
- Análisis semántico: comprueba lo que la gramática no puede ver, sobre todo los tipos: que no se sume un texto a un número, que la variable esté declarada, que la función reciba los argumentos que pide.
- Generación de código intermedio: traduce el árbol a una representación interna sencilla, independiente todavía del procesador de destino.
- Optimización: reescribe ese código intermedio para que haga lo mismo con menos trabajo, por ejemplo sacando de dentro de un bucle un cálculo que siempre da lo mismo.
- Generación de código objeto: produce por fin las instrucciones máquina del procesador concreto.
El resultado de compilar todavía no es un programa ejecutable, sino un módulo objeto con huecos: las llamadas a funciones que están en otros archivos o en bibliotecas del sistema aparecen como referencias sin resolver. De rellenar esos huecos se encarga el enlazador (linker), que junta los módulos objeto y las bibliotecas necesarias y produce el ejecutable final. Ya en ejecución, el cargador es quien lo lleva a memoria y lo pone en marcha.
El enlazado puede ser estático, cuando el código de la biblioteca se copia dentro del ejecutable (que queda grande pero autosuficiente), o dinámico, cuando la biblioteca se carga aparte en el momento de la ejecución. El enlazado dinámico ahorra espacio y permite actualizar la biblioteca sin recompilar los programas que la usan, a cambio de que su ausencia en el sistema rompa el programa.
El compilador produce módulos objeto con referencias sin resolver; es el enlazador el que produce el ejecutable. Confundir los dos es el fallo habitual en este apartado.
Para el examen
Las fases: análisis léxico, sintáctico y semántico, código intermedio, optimización y código objeto
Dónde se cazan los tipos: en el análisis semántico
Enlazador: junta módulos y bibliotecas y produce el ejecutable
Cargador: lo lleva a memoria