Los siete ur-lenguajes de la programación (2022)
(madhadron.com)- Las diferencias entre conjuntos de patrones fundamentales importan más que la gramática individual, y los lenguajes de programación se dividen en siete ur-lenguajes según su forma de iteración, recursión y composición.
- ALGOL, Lisp, ML, Self, Forth, APL y Prolog son las categorías centrales, y cada familia usa un lenguaje representativo como muestra de referencia para juzgar el linaje de otros lenguajes.
- Un lenguaje nuevo que comparte un ur-lenguaje familiar es fácil de aprender, pero al pasar a un ur-lenguaje desconocido se necesitan nuevas rutas de pensamiento y bastante tiempo de aprendizaje.
- ALGOL se caracteriza por la organización de funciones centrada en asignaciones, condicionales y bucles; Lisp por los macros y el código como listas; ML por las funciones de primera clase y la recursión; Self por los objetos con paso de mensajes; Forth por la sintaxis basada en pila; APL por los arreglos n-dimensionales; y Prolog por las estructuras de hechos y búsqueda.
- Para todo programador, la prioridad es dominar un lenguaje de la familia ALGOL; después aprender SQL, y luego seguir estudiando ur-lenguajes desconocidos de forma constante resulta ventajoso a largo plazo.
Los siete ur-lenguajes de la programación
- Al elegir un lenguaje de programación, es más importante adquirir patrones fundamentales que fijarse en diferencias individuales de sintaxis; entre lenguajes de una familia parecida, estructuras básicas como recorrer arreglos o recorrer combinaciones suelen tener casi la misma forma.
- Los distintos grupos de lenguajes difieren mucho en cómo hacen iteración, recursión y composición de programas, y estos conjuntos de patrones fundamentales forman ur-lenguajes distintos.
- Aprender un lenguaje nuevo que comparte un ur-lenguaje conocido es una transición relativamente fácil, pero moverse a un ur-lenguaje desconocido exige bastante tiempo y nuevas rutas de pensamiento.
- En software, los ur-lenguajes que se reconocen son siete: ALGOL, Lisp, ML, Self, Forth, APL y Prolog.
- Cada ur-lenguaje se clasifica tomando cierto lenguaje representativo como una muestra de referencia, y otros lenguajes se juzgan por comparación con esa muestra.
-
ALGOL
- Un programa se compone como una secuencia de asignaciones, condicionales y bucles, organizada por funciones.
- Muchos lenguajes agregan aquí sistemas de módulos, formas de definir nuevos tipos de datos, polimorfismo y estructuras alternativas de control de flujo como excepciones o corrutinas.
- La mayoría de los lenguajes de programación ampliamente usados hoy pertenecen a esta familia de ur-lenguajes.
- En el propio ALGOL se incluyen ALGOL 58, ALGOL 60, ALGOL W y ALGOL 68.
- Assembly language, Fortran, C, C++, Python, Java, C#, Ruby, Pascal, JavaScript y Ada están conectados con esta familia.
- Es el ur-lenguaje más antiguo, con un linaje que se remonta hasta la formalización de programas de Ada Lovelace para la máquina analítica de Babbage.
- El lenguaje máquina y el ensamblador de las computadoras con arquitectura Eckert-Mauchly que llevaron a EDVAC y las primeras Univac, junto con los primeros intentos de lenguajes de alto nivel desde A-0 de Grace Hopper hasta Fortran y COBOL, comparten esta forma.
- En la academia de los años 60 se desarrolló la programación estructurada para hacer estos lenguajes más manejables, y el resultado fue ALGOL 60, del que luego derivó la mayoría de los miembros de esta familia.
- Con el tiempo ha tendido a absorber funciones de otros ur-lenguajes.
- En los años 80, conceptos de la familia Self se incorporaron en forma de clases y se usaron como medio para implementar definición de tipos de datos y polimorfismo.
- Después de 2010 también aparecieron conceptos de la familia ML.
-
Lisp
- Una sintaxis que combina expresiones prefijas entre paréntesis y representación por listas.
(+ 2 3)(defun square (x) (* x x))(* (square 3) 3)
- La representación por listas de elementos separados por espacios y rodeados por paréntesis está integrada en el lenguaje, así que el propio código tiene forma de lista.
- Como los macros pueden recibir listas, modificarlas y luego pasar el código modificado al compilador, el programador puede redefinir la semántica del lenguaje.
- En la mayor parte de la escritura de código, suele comportarse como otros ur-lenguajes, normalmente ALGOL o ML, pero el sistema de macros es su rasgo distintivo.
- La sintaxis
loopde Common Lisp tampoco es una función integrada del lenguaje, sino que está definida como macro. - Hubo muchas variantes tempranas de Lisp, pero la comunidad alcanzó un consenso alrededor de Common Lisp.
- Sussman y Steele exploraron hasta dónde se podía llegar solo con funciones y crearon Scheme.
- También existen Lisps de propósito específico como Lush para cálculo numérico, AutoLISP como lenguaje de scripting de AutoCAD y Emacs Lisp para implementar el comportamiento del editor Emacs.
- Más recientemente, Clojure ha surgido como la tercera gran rama de la familia Lisp.
- Es la segunda familia de lenguajes más antigua que sigue en uso hoy, aparecida aproximadamente un año después de Fortran.
- Su punto de partida fue una pregunta matemática sobre cómo expresar una estructura matemática capaz de evaluar sus propias expresiones.
- John McCarthy propuso una respuesta en 1958 y luego se implementó en computadoras.
- Por su trasfondo matemático, el Lisp temprano no encajaba bien con las máquinas de la época; problemas como memoria y ciclos de CPU no existían en matemáticas, y se necesitaron técnicas como la recolección de basura.
- A finales de los 70 y principios de los 80 existieron máquinas diseñadas desde cero solo para ejecutar Lisp.
- Muchos elementos de los entornos de desarrollo integrados actuales se inventaron en esas máquinas.
- En la misma época, Lisp era la herramienta principal de la investigación en inteligencia artificial, y cuando el auge de la IA de los años 80 no produjo resultados, Lisp cayó junto con el campo durante el AI Winter.
- Aun así sobrevivió, y con el aumento del rendimiento de las computadoras y la adopción de sus funciones por otros lenguajes, sus dificultades de implementación se redujeron.
- Una sintaxis que combina expresiones prefijas entre paréntesis y representación por listas.
-
ML
- Las funciones son valores de primera clase, y posee un sistema de tipos de la familia Hindley-Milner capaz de expresar varias funciones y uniones etiquetadas.
- Toda repetición se realiza mediante recursión.
sum [] = 0sum (x:xs) = x + sum xs
- También se usa el enfoque de definir funciones que encapsulan patrones de repetición y reciben otras funciones para implementar el comportamiento.
map _ [] = []map f (x:xs) = (f x) : (map f xs)
- Algunos lenguajes, como Miranda y Haskell, usan evaluación perezosa por defecto.
- Otros lenguajes extienden el sistema de tipos en varias direcciones.
- OCaml intenta combinarse con conceptos del ur-lenguaje Self.
- Agda e Idris adoptan sistemas de tipos dependientes que mezclan valores y tipos.
- 1ML combina módulos y tipos.
- De ML derivan CaML, Standard ML y OCaml.
- También continúan familias relacionadas como Miranda, Haskell, Agda e Idris.
- ML era el metalenguaje de un programa de demostración de teoremas desarrollado en Cambridge, Reino Unido, y de ahí viene su nombre.
- Después se extendió más allá de ese contexto como lenguaje independiente y ganó popularidad en Europa, especialmente en Reino Unido y Francia.
-
Self
- Un programa está formado por un conjunto de objetos que se envían mensajes entre sí, y todo el comportamiento se implementa de esa manera.
- Los objetos nuevos se crean enviando mensajes a objetos existentes.
- Incluso los condicionales se realizan a través de una variable que referencia ya sea al objeto true o al objeto false.
- Los dos objetos reciben un mensaje con como parámetros la función a ejecutar si es verdadero y la función a ejecutar si es falso.
- El objeto true ejecuta la primera función y el objeto false ejecuta la segunda.
- El código que llama no sabe cuál objeto es y solo envía un mensaje.
- Los bucles funcionan igual, y si se crea el objeto adecuado y se coloca en el lugar adecuado, es posible redefinir por completo la semántica del lenguaje.
- Estos lenguajes normalmente no guardan el código fuente como archivos de texto, sino en un entorno en vivo.
- El programador modifica el sistema en vivo y guarda ese estado, en vez de compilar archivos para construir el sistema.
- Los ejemplos importantes son Smalltalk y Self.
- Muchos lenguajes adoptan solo parcialmente el paso de mensajes de esta familia, y a esa adopción parcial normalmente se le llama programación orientada a objetos.
- La mayoría de ellos se basan en Smalltalk, y JavaScript es la única excepción, pues deriva del sistema de objetos sin clases de Self.
- El sistema de objetos de Common Lisp generaliza esto para que el runtime elija el código a ejecutar no solo según un objeto receptor del mensaje, sino según todos los parámetros.
- Erlang cambia de dirección: en vez de que el flujo de ejecución se desplace entre objetos, hace que hilos de ejecución en paralelo escuchen y envíen mensajes explícitamente.
- El lenguaje original es Smalltalk, desarrollado en Xerox Parc a finales de los 70 y en los 80.
- En los años 80 hubo varios sistemas comerciales de Smalltalk, e IBM usó Smalltalk para desarrollar la colección VisualAge de herramientas de programación para otros lenguajes.
- Hoy Smalltalk sobrevive principalmente en la forma del proyecto open source Pharo Smalltalk.
- Se investigó mucho cómo ejecutar Smalltalk de forma rápida y eficiente, y el punto culminante fue el proyecto Strongtalk.
- Los hallazgos de Strongtalk tienen importancia histórica porque fueron la base del compilador JIT HotSpot de Java.
- Smalltalk heredó de lenguajes anteriores las ideas de valor y tipo para implementar clases: todos los objetos tenían una clase que les daba tipo, y la clase creaba el objeto de ese tipo.
- Self eliminó el concepto de clase y quedó compuesto solo por objetos.
- Por ser una forma más pura, se elige Self como muestra representativa de este ur-lenguaje.
-
Forth
- Los lenguajes de pila son una especie de imagen especular de Lisp, y comparten sintaxis con las calculadoras de notación polaca inversa de Hewlett Packard.
- Tienen una pila de datos, y al escribir un literal como
42este se empuja a la pila; los nombres de funciones operan sobre la pila sin parámetros explícitos. - Incluso la aritmética simple aparece invertida, como
2 3 + 5 *. - La definición de funciones también es muy concisa.
- En la mayoría de las variantes de Forth,
:define una palabra nueva. squareequivale a llamardupy*.dupduplica el elemento superior de la pila y*multiplica los dos elementos superiores.
- En la mayoría de las variantes de Forth,
- Se puede interceptar el parser y reemplazarlo con código propio, así que es posible sustituir toda la sintaxis.
- Son comunes los programas en Forth que definen pequeños lenguajes, como subconjuntos de Fortran, layouts de paquetes o formas de parsear directamente diagramas ASCII que representan transiciones de máquinas de estado.
- Incluye varias variantes de Forth, además de PostScript, Factor y Joy.
- Joy es un lenguaje funcional puro que usa una formalización matemática de la composición en lugar de pila.
- Forth se escribió por primera vez en 1970 para el control de radiotelescopios.
- Después se difundió ampliamente por los sistemas embebidos.
- Los sistemas Forth son lo bastante fáciles de bootstrapear como para que existan decenas de variantes hechas por programadores para sus propios fines.
- PostScript apareció en los años 80 como una forma flexible de describir documentos en impresoras.
- En muchos sentidos PostScript es más restringido que Forth, pero define en el lenguaje operaciones básicas relacionadas con layout gráfico.
-
APL
- Todo en el lenguaje son arreglos n-dimensionales.
- Los operadores están formados por uno o dos símbolos y realizan operaciones de alto nivel sobre arreglos completos.
- La expresión es tan compacta que la propia secuencia de símbolos sirve como marca de la operación sin necesidad de ponerle otros nombres.
- Por ejemplo, calcular el promedio de la variable
xse escribe como(+⌿÷≢) x. - APL, J y K son ejemplos representativos.
- Las operaciones de orden superior sobre arreglos se han exportado parcialmente a muchos entornos como MATLAB, NumPy y R.
- APL comenzó como una notación matemática creada por Kenneth Iverson en los años 60 y luego se implementó en computadoras.
- Desde entonces ha mantenido un nicho de seguidores entre quienes realizan cálculos pesados.
- Su lenguaje descendiente K fue muy popular en el entorno financiero.
-
Prolog
- Un programa se compone de un conjunto de hechos.
father(bob, ed).father(bob, jane).
- También se usan hechos no instanciados que derivan hechos a partir de otros mediante variables.
grandfather(X, Y) :- father(X, Z), father(Z, Y).
- El runtime de Prolog toma estos hechos y consultas y realiza una búsqueda para encontrar resultados.
- Si se elige correctamente la estructura de definición de hechos, se obtiene completitud de Turing.
- En Prolog, los términos que forman los hechos son en sí mismos un tipo de dato distintivo que puede crearse y pasarse al runtime.
- En ese sentido ocupa una posición parecida a la de los macros en Lisp o el reemplazo del parser en Forth.
- Como los programas en Prolog son esencialmente búsqueda, el ajuste se centra en controlar el orden de búsqueda y cortar pronto los caminos sin resultados, como en las consultas a bases de datos.
- Incluye Prolog, Mercury y Kanren.
- La mayor parte de la programación real en esta familia de ur-lenguajes ocurre en el propio Prolog, y la comunidad está muy unificada.
- En los años 70, lógicos franceses se dieron cuenta de que los programas podían expresarse con lógica de primer orden y empezaron a intentar implementarlo.
- En los años 80, el proyecto japonés de computadoras de quinta generación apostó fuerte por Prolog, pero al fracasar el proyecto también cayó la reputación de Prolog.
- Aun así, durante décadas se ha investigado cómo hacer que los runtimes de Prolog sean eficientes en la mayoría de los casos y cómo añadir nuevas funciones.
- Se agregaron capacidades como las restricciones numéricas, lo que llevó a la programación lógica con restricciones.
- Prolog sigue apareciendo en nichos concretos.
- La verificación de tipos de Java estuvo implementada en Prolog durante varios años.
- La primera herramienta de búsqueda de código fuente de Facebook también estaba basada en Prolog.
- Un programa se compone de un conjunto de hechos.
Cómo aprovecharlo
- Para la mayoría de los programadores, algunas o todas estas familias de lenguajes pueden parecer muy ajenas, pero vale la pena invertir tiempo en ellas por las rutas de pensamiento y nuevas posibilidades que abren.
- Es muy común que dos cosas que desde una perspectiva ALGOL parecen totalmente distintas, desde otra perspectiva resulten una comparación trivial.
-
Prioridades
- Todo programador debería conocer bien al menos un lenguaje de la familia ALGOL.
- Después se recomienda aprender SQL, un lenguaje de la familia Prolog.
- En una carrera profesional, su utilidad suele ser la segunda más grande después de ALGOL.
-
Expansión posterior
- Después de dominar esas dos familias, conviene a largo plazo aprender cada año un lenguaje nuevo de una familia de ur-lenguajes desconocida.
- Los lenguajes sugeridos en cada familia y el orden propuesto son los siguientes.
- Lisp: PLT Racket
- ML: Haskell
- Self: Self
- Prolog: Prolog
- Forth: gForth
- APL: K, usado mediante
ok
-
Ajustar el orden
- Si haces mucho cálculo numérico, conviene aprender K más temprano.
- Si haces mucha programación embebida, conviene aprender gForth más temprano.
- Pero el orden en sí ni exactamente qué lenguaje elegir es lo más importante.
- En esa categoría, no pasa nada si aprendes Standard ML u OCaml en lugar de Haskell, Common Lisp en lugar de PLT Racket, o Factor en lugar de gForth.
-
Complementos incluidos en las notas
- Incluso después de aprender SQL, sigue siendo necesario aprender Prolog en sí.
- Porque la forma real de usarlo es bastante distinta de SQL.
- Se incluye la opinión de un lector de que una forma común de entender Forth a fondo es construir uno mismo una implementación de Forth.
- Se menciona que Forth es lo bastante pequeño como para que una sola persona pueda implementarlo desde cero en relativamente poco tiempo.
- gForth es una buena implementación para aprender ANS Forth.
- Como material de estudio se menciona FORTH Fundamentals, Volume 1 de McCabe.
- También se mencionan PygmyForth, eForth y colorForth como otros Forth para revisar.
- Incluso después de aprender SQL, sigue siendo necesario aprender Prolog en sí.
5 comentarios
Está interesante.
En la universidad aprendí materias de la carrera e hice tareas con la familia ALGOL, Lisp y Prolog, así que me trae muchos recuerdos.
Esos lenguajes dejaron mucho en los lenguajes de programación modernos de uso general,
pero de entre ellos, solo Forth parece haber tenido menos influencia.
Aunque no conozcas la notación prefija, programar con notación posfija sí que resulta demasiado incómodo.
Comentarios de Hacker News
En la clase de PL de Tufts hice versiones mini, una por una, de las primeras 4 familias de lenguajes antes de imperativo, Lisp, ML, Smalltalk, y me dio gusto ver que ese proceso ahora también salió como libro de texto. Sí da pena que hayan quitado la parte de Prolog, porque antes sí estaba
Si tuviera que corregir una sola cosa de la clasificación de este texto, diría que Ruby no es tanto de la familia Algol como claramente un lenguaje orientado a objetos. La influencia de Smalltalk es grande, y hasta en nombres de la biblioteca estándar queda esa huella, con cosas como
collecten vez demap. En Ruby, de principio a fin, todo es un objeto, y lo natural es entender las llamadas a métodos como enviar mensajes a objetos. Se compara mucho con Python, pero su ruta evolutiva fue bastante distinta, y ahora da la impresión de que convergieron hacia ecosistemas parecidos. Para mí, Ruby se siente como una alpaca más acogedora que PythonHello Worldse nota menos, pero incluso los tipos básicos pasaron a ser objetos. Si quieres remarcarle eso a alguien que no soporta OOP, basta mostrarletype(42)ydir(42)para dejar claro que hasta un entero es un objetoYo añadiría una categoría más al árbol genealógico de lenguajes: los lenguajes para expresar pruebas. Son los de la correspondencia Curry-Howard, donde el programa es la prueba, y Lean sería un ejemplo representativo. Podría verse como una subcategoría de lo funcional, pero siento que vale la pena tratarlo como otro eje, porque su objetivo principal es la verificación más que la ejecución
Hace poco volví a revisar un proyecto de comparación de lenguajes: era un benchmark que descomponía cíclicamente en paralelo las 3,715,891,200 signed permutations de 10 caracteres. Más que “lenguajes prototipo”, quería encontrar implementaciones modernas de cada paradigma que de verdad se pudieran elegir para programación de investigación. No solo miré rendimiento, también qué tan fácil era recibir ayuda de IA y qué tan cómodo me resultaba leer y pensar el código; gracias a la IA, además, se pudo hacer una especie de turismo optimizando cada lenguaje con bastante profundidad. Los resultados están aquí, y en particular me sorprendió mucho que F# quedara hasta arriba
Yo también escribí algo parecido aquí. Coincido con Algol, Lisp, Forth, APL y Prolog, pero para lenguaje funcional innovador puse SASL, que es un poco anterior a ML, y como representante orientado a objetos elegí Smalltalk, que salió antes que Self. Además incluí Fortran, COBOL, SNOBOL y Prograph, porque me parecen lenguajes que cada uno cambió el panorama a su manera
Yo añadiría a esta discusión más familias semánticas. Cosas como Verilog, Petri nets, Kahn process networks, dataflow machines, process calculi, reactive, term rewriting, constraint solver/theorem prover, y probabilistic programming. También pienso en lenguajes como Unison, Darklang, temporal dataflow y DBSP, que no encajan exactamente en esas 7 categorías pero en la práctica están cerca de producción. Puede parecer un poco tramposo, pero en esencia se trata de modelos de cómputo paralelos al modelo de máquina de von Neumann. Desde hace tiempo me gustaría escribir algo como “todas las formas de computación que conocemos, más allá de von Neumann”
1+1como algo tipoADD(1,1), ya podía parsearlo de una forma que sí entendía. Encima me negué tontamente a aprender regex, así que el código quedó bastante raro, y todavía me acuerdo de un compañero diciendo “como Andy dice que funciona, mejor no lo toquemos”. Gente de otro equipo resolvió lo mismo con regex en un código como 20 veces más corto que el mío“Concepts of programming languages”, que tomé en TU Delft, fue mi materia favorita de toda la carrera de computación. Vimos C, Scala del lado funcional, y JavaScript por el concepto de prototipos, y gracias a eso años después aprender Elixir me resultó mucho más fácil. También había una clase donde programábamos agentes de Unreal Tournament en GOAL, un lenguaje basado en Prolog. Durante mucho tiempo no supe bien dónde usar Prolog, pero al final lo terminé usando para hacer un spellcheck que corrigiera iterativamente frases pésimas en papiamento generadas por un LLM
Estoy de acuerdo con la idea de que hay que aprender lenguajes de clases distintas. No fue hasta que aprendí OCaml que las funciones empezaron a sentirse de verdad como funciones matemáticas, y Mathematica me enseñó a ver las expresiones mismas como entradas. La notación polaca inversa de PostScript no solo me sirvió para aritmética simple: literalmente me recableó la forma de pensar. Pero no estoy de acuerdo con decir que da igual elegir entre Java, C#, C++, Python o Ruby. Si tu meta es algo como implementar quicksort, quizá sí se parezcan, pero para alguien que realmente quiere construir cosas, la elección del lenguaje puede marcar una diferencia tan grande como de día y noche. Si a alguien que quiere hacer juegos 3D le das Ruby, o a alguien que quiere hacer ciencia de datos exploratoria o deep learning le das Java como primer lenguaje, puedes matarle la motivación
Este artículo me hizo pensar en 7 languages in 7 weeks de Bruce Tate. Yo también conocí Erlang por ese libro. Aun así, históricamente me parece un poco forzado meter a COBOL y Fortran dentro de la familia Algol, aunque al mismo tiempo sí recuerda que la historia inevitablemente tiene algo de simplificación reductiva
Relacionado con esto, ya hubo una discusión vieja en HN. Ver la discusión anterior también ayuda a entender el contexto