Los siete ur-lenguajes de la programación (2022)
(madhadron.com)- La diferencia entre los conjuntos de patrones fundamentales es más importante que la gramática individual, y los lenguajes de programación se dividen en siete ur-lenguajes según cómo manejan la iteración, la recursión y la composición.
- ALGOL, Lisp, ML, Self, Forth, APL y Prolog son la clasificación central, 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 pasar a un ur-lenguaje desconocido requiere nuevas rutas de pensamiento y bastante tiempo de aprendizaje.
- ALGOL se caracteriza por la organización de funciones centrada en asignación, 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 una sintaxis basada en pila; APL por los arreglos n-dimensionales; y Prolog por los hechos y la estructura de búsqueda.
- Para cualquier programador, dominar primero un lenguaje de la familia ALGOL es prioritario; luego conviene aprender SQL, y después seguir estudiando de forma constante ur-lenguajes desconocidos, lo que resulta ventajoso a largo plazo.
Los siete ur-lenguajes de la programación
- Al elegir un lenguaje de programación, es más importante aprender patrones fundamentales que diferencias de gramática individuales, y entre lenguajes de familias similares las estructuras básicas como recorrer arreglos o combinaciones suelen tener casi la misma forma.
- Los distintos grupos de lenguajes difieren mucho en cómo manejan la iteración, la recursión y la organización del programa, y estos conjuntos de patrones fundamentales forman ur-lenguajes distintos.
- Aprender un lenguaje nuevo que comparte un ur-lenguaje familiar es una transición relativamente fácil, pero moverse a un ur-lenguaje desconocido exige bastante tiempo y nuevas rutas de pensamiento.
- En el software, los ur-lenguajes reconocidos son siete: ALGOL, Lisp, ML, Self, Forth, APL y Prolog.
- Cada ur-lenguaje se clasifica usando un lenguaje representativo específico como muestra de referencia, y los demás lenguajes se ubican comparándolos con esa muestra.
-
ALGOL
- El programa está formado por una secuencia de asignaciones, condicionales y bucles, organizada en 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 corutinas.
- La mayoría de los lenguajes de programación ampliamente usados hoy pertenecen a la familia de este ur-lenguaje.
- Dentro de ALGOL mismo se incluyen ALGOL 58, ALGOL 60, ALGOL W y ALGOL 68.
- Assembly language, Fortran, C, C++, Python, Java, C#, Ruby, Pascal, JavaScript y Ada se conectan con esta familia.
- Es el ur-lenguaje más antiguo, con un linaje que se remonta hasta la formalización de programas para la máquina analítica de Babbage por Ada Lovelace.
- El lenguaje máquina y el ensamblador de las computadoras con arquitectura Eckert-Mauchly, que llevaron a EDVAC y las primeras Univac, así como los primeros intentos de lenguajes de alto nivel desde A-0 de Grace Hopper hasta Fortran y COBOL, tienen todos 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; después, la mayoría de los miembros de la familia derivaron de ahí.
- Con el tiempo, ha habido una tendencia 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.
- Desde 2010 también aparecieron conceptos de la familia ML.
-
Lisp
- Su sintaxis combina expresiones prefijas entre paréntesis con la representación por listas.
(+ 2 3)(defun square (x) (* x x))(* (square 3) 3)
- La representación por listas entre paréntesis, con elementos separados por espacios, está integrada en el lenguaje, así que el propio código tiene forma de lista.
- Los macros pueden recibir listas, modificarlas y pasar el código modificado al compilador, de modo que el programador puede redefinir el significado 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.
- Incluso la sintaxis
loopde Common Lisp no es una función integrada del lenguaje, sino una forma definida mediante macros. - Hubo muchas variantes tempranas de Lisp, pero la comunidad llegó a un acuerdo en torno a 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 especial como Lush para cálculo numérico, AutoLISP como lenguaje de scripts 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 alrededor de un año después de Fortran.
- Su punto de partida fue una pregunta matemática sobre cómo representar una estructura matemática capaz de evaluar sus propias expresiones.
- John McCarthy dio una respuesta en 1958 y luego se implementó en computadoras.
- El Lisp temprano, por su trasfondo matemático, no encajaba bien con las máquinas de la época; problemas de memoria y ciclos de CPU no existían en matemáticas, y se volvieron necesarias técnicas como la recolección de basura.
- A fines de los 70 y comienzos 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 ese mismo periodo, Lisp fue el principal medio de investigación en inteligencia artificial, y cuando el auge de la IA de los años 80 no dio resultados, Lisp también cayó junto con el campo en el invierno de la IA.
- Aun así sobrevivió, y con el aumento del rendimiento de las computadoras y la adopción de sus funciones por otros lenguajes, las dificultades de implementación se redujeron.
- Su sintaxis combina expresiones prefijas entre paréntesis con la representación por listas.
-
ML
- Las funciones son valores de primera clase, y el lenguaje cuenta con un sistema de tipos de la familia Hindley-Milner capaz de expresar varias funciones y uniones etiquetadas.
- Toda iteració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 iteració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 diferida por defecto.
- Otros expanden 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 siguen familias relacionadas como Miranda, Haskell, Agda e Idris.
- ML fue 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 y se difundió como lenguaje independiente, ganando popularidad en Europa, especialmente en Reino Unido y Francia.
-
Self
- El programa está compuesto por un conjunto de objetos que se envían mensajes entre sí, y todo comportamiento se implementa de esa manera.
- Los objetos nuevos se crean enviando mensajes a objetos existentes.
- Incluso los condicionales se realizan mediante una variable que apunta a uno de dos objetos, true o false.
- Esos dos objetos reciben un mensaje con como parámetros una función para ejecutar si es verdadero y otra 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 qué objeto es; simplemente envía un mensaje.
- Los bucles funcionan igual, y si se crean los objetos adecuados y se colocan en los lugares adecuados, es posible redefinir por completo el significado del lenguaje.
- Estos lenguajes normalmente guardan el código fuente no en archivos de texto sino en un entorno vivo.
- El programador modifica el sistema en vivo y, en lugar de compilar archivos para construir el sistema, guarda ese estado.
- 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 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 el objeto que recibe el mensaje, sino según todos los parámetros.
- Erlang, en vez de mover el flujo de ejecución entre objetos, gira hacia hilos de ejecución paralelos que escuchan y envían mensajes de manera explícita.
- El lenguaje original es Smalltalk, desarrollado en Xerox Parc a fines 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 de Pharo Smalltalk de código abierto.
- Se investigó mucho cómo ejecutar Smalltalk de forma rápida y eficiente, y el punto más alto fue el proyecto Strongtalk.
- Los hallazgos de Strongtalk tienen importancia histórica porque sirvieron de base para el compilador JIT HotSpot de Java.
- Smalltalk heredó de lenguajes anteriores las ideas de valor y tipo para implementar clases: todo objeto tenía una clase que le daba un tipo, y la clase creaba los objetos de ese tipo.
- Self eliminó el concepto de clase y quedó compuesto solo por objetos.
- Por ser una forma más pura, Self fue elegido como muestra representativa de este ur-lenguaje.
-
Forth
- Los lenguajes de pila son una especie de imagen invertida de Lisp y comparten sintaxis con las calculadoras de notación polaca inversa de Hewlett Packard.
- Tienen una pila de datos; cuando se escribe un literal como
42, se hace push en la pila, y los nombres de funciones operan sobre esa pila sin parámetros explícitos. - Incluso la aritmética simple aparece invertida, como
2 3 + 5 *. - Las definiciones de funciones también tienen una forma muy concisa.
- En la mayoría de las variantes de Forth,
:define una nueva palabra. squareequivale a llamar adupy*.dupduplica el elemento superior de la pila, y*multiplica los dos elementos superiores.
- En la mayoría de las variantes de Forth,
- Puede interceptar el parser y reemplazarlo con su propio código, así que es posible sustituir toda la sintaxis.
- Es común encontrar programas en Forth que definen pequeños lenguajes, como subconjuntos de Fortran o formas de parsear directamente diagramas ASCII que representan layouts de paquetes o transiciones de máquinas de estados.
- Incluye varias variantes de Forth, además de PostScript, Factor y Joy.
- Joy es un lenguaje puramente funcional que usa una formulación matemática de la composición en lugar de una pila.
- Forth se escribió por primera vez en 1970 para el control de radiotelescopios.
- Después se expandió ampliamente por los sistemas embebidos.
- Los sistemas Forth son lo bastante fáciles de bootstrapear como para que existan decenas de variantes hechas por distintos programadores para sus propios fines.
- PostScript apareció en los años 80 como un medio flexible para describir documentos en impresoras.
- PostScript es más restringido que Forth en varios aspectos, pero define en el lenguaje operaciones básicas relacionadas con el layout gráfico.
-
APL
- Todo en el lenguaje son arreglos n-dimensionales.
- Los operadores se componen de 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 funciona como marca de la operación sin necesidad de darle otro nombre.
- Por ejemplo, calcular el promedio de la variable
xtoma la forma(+⌿÷≢) x. - APL, J y K son ejemplos representativos.
- Las operaciones de alto orden 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 una base de apoyo de nicho entre quienes realizan cómputo pesado.
- Su lenguaje descendiente K fue muy popular en entornos financieros.
-
Prolog
- El programa está formado por un conjunto de hechos.
father(bob, ed).father(bob, jane).
- También se usan hechos no aterrizados que derivan otros hechos 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 adecuadamente 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 único, que puede construirse y pasarse al runtime.
- En eso ocupa una posición similar a los macros de Lisp o al reemplazo del parser en Forth.
- Como los programas en Prolog son esencialmente búsqueda, su ajuste se centra en controlar el orden de búsqueda y cortar temprano los caminos sin resultados, como en una consulta a base de datos.
- Incluye Prolog, Mercury y Kanren.
- La mayor parte de la programación real en esta familia de ur-lenguajes se hace en Prolog mismo, y la comunidad mantiene una unidad muy fuerte.
- En los años 70, lógicos franceses se dieron cuenta de que los programas podían expresarse en 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 con el fracaso del proyecto también cayó la reputación de Prolog.
- Aun así, durante décadas siguió la investigación para hacer más eficientes los runtimes de Prolog en la mayoría de los casos y para agregar nuevas funciones.
- Al sumarse funciones como las restricciones numéricas, esto llevó a la programación lógica con restricciones.
- Prolog sigue apareciendo en áreas de nicho.
- La comprobación de tipos de Java estuvo implementada en Prolog durante varios años.
- La herramienta temprana de búsqueda de código fuente de Facebook también se basó en Prolog.
- El programa está formado por un conjunto de hechos.
Cómo aprovecharlo
- Para la mayoría de los programadores, algunos o todos estos grupos de lenguajes pueden parecer muy ajenos, pero vale la pena invertir tiempo en cada uno por las rutas de pensamiento y las nuevas posibilidades que abren.
- Desde la perspectiva de ALGOL, dos cosas pueden parecer completamente distintas, pero desde otra perspectiva muy a menudo la comparación es trivial.
-
Prioridad
- Todo programador debería conocer bien un lenguaje de la familia ALGOL.
- Después se recomienda aprender SQL, un lenguaje de la familia Prolog.
- Ocupa el lugar de mayor utilidad en la carrera después de ALGOL.
-
Expansión posterior
- Después de dominar esas dos familias, a largo plazo conviene aprender cada año un lenguaje nuevo de una familia de ur-lenguaje desconocida.
- Los lenguajes sugeridos y el orden para cada familia tienen esta forma:
- Lisp: PLT Racket
- ML: Haskell
- Self: Self
- Prolog: Prolog
- Forth: gForth
- APL: K, usando
ok
-
Ajuste del 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í o exactamente qué lenguaje elegir no es lo importante.
- Está perfectamente bien aprender Standard ML u OCaml en vez de Haskell, Common Lisp en vez de PLT Racket, o Factor en vez de gForth.
-
Complementos incluidos en las notas
- Incluso después de aprender SQL, sigue siendo necesario aprender Prolog mismo.
- Porque su forma real de uso es bastante distinta a SQL.
- Se incluye la opinión de un lector de que una forma común de entender Forth en profundidad es crear 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 aprendizaje se menciona FORTH Fundamentals, Volume 1 de McCabe.
- También se mencionan PygmyForth, eForth y colorForth como Forths para revisar.
- Incluso después de aprender SQL, sigue siendo necesario aprender Prolog mismo.
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