Recientemente una amiga que comienza en el desarrollo de software me preguntó (y parafraseo)

¿Existe algún lenguaje de programación en que los errores sean prácticamente nulos? Alguno en donde, a pesar de no poner un punto y coma o una comilla mal, aún así compile.

Y es que probablemente es algo que todos los desarrolladores hemos deseado en algún punto de nuestra existencia.

No conozco a nadie que no se haya sentido frustrado jamás por no lograr encontrar el error que está previniendo que nuestro código sea aceptado por el compilador, y además los mensajes de error que emite el análisis sintáctico no resultan de ayuda.

En un mundo en donde pareciera que las computadoras son mejores que los humanos en prácticamente cualquier tarea, uno pensaría que la misma computadora debería ser igualmente capaz de resolver el problema que nosotros como programadores queremos solucionar. Y por lo mismo, pareciera natural hacernos la pregunta: ¿por qué el compilador no resuelve el problema?

No me considero en ninguna manera experto en el diseño de lenguajes de programación o su implementación mediante intérpretes y/o compiladores. Sin embargo, creo poder proporcionar algunos argumentos suficientemente convincentes sobre el por qué no existen tales lenguajes que etiqueto como “inexactos”.

Primero que nada entendamos a qué me refiero por un lenguaje inexacto. Queremos un lenguaje de programación que sea capaz de resolver errores sintácticos. Por ahora únicamente pensemos en errores sintácticos “menores” (si es que siquiera podemos clasificar los niveles de un error), es decir, algún tipo de puntuación faltante como punto y coma, o paréntesis; un typo en el nombre de una variable, etc. Entiendo que siendo estrictos esta no es una definición bien establecida y mi matemático interior está en descontento, pero por ahora bastará la intuición, y no tengo duda de que con más tiempo y herramientas esto se pueda modelar de una manera más formal para proveer un argumento más satisfactorio.

Pensemos en el lenguaje Python, que para muchos (yo incluido) es el lenguaje de programación más amigable y sencillo de aprender que existe. ¿Cómo se vería un dialecto “inexacto” de este lenguaje? Pues seguro cada desarrollador podrá pensar en alguno distinto según los errores que tenga en un determinado momento, pero por ahora yo propongo lo siguiente:

suma = 0
for x in range(101)
    suam += x
  print("Los primeros 100 números naturales suman: {}".format(suma)

Probablemente ya pudiste identificar algunos errores con mi código:

  1. Me faltó un : al final de la segunda línea, después de range(101).
  2. En la línea 3 escribí suam en vez de suma.
  3. Utilicé 4 espacios como indentación en la línea 3, y 2 espacios en la línea 4.
  4. Me faltó cerrar el paréntesis de print en la línea 4.

Ahora conviene pensar en la siguiente pregunta: ¿para cuáles de los errores anteriores se puede identificar la solución correcta de manera meramente sintáctica? En otras palabras, si no tuviéramos conocimiento del algoritmo que estamos implementando, y nuestra única forma de identificar errores es analizando el texto que conforma el código, ¿qué errores podríamos resolver?

Probablemente habrá una variedad de opiniones con respecto a la pregunta anterior. La opinión más extrema sería que ningún error se puede resolver sin tomar en cuenta la semántica, y sinceramente creo estar de acuerdo. Sin embargo, con la finalidad de no acabar aquí el post, voy a tomar una postura un poco más flexible.

Usemos la siguiente clasificación para los 4 errores anteriormente enumerados:

  1. Error solucionable por medios sintácticos: La computadora deduce que, como estamos creando un bloque for y la siguiente línea tiene indentación mayor, nos hace falta : para solucionar el error.
  2. Error no solucionable sintácticamente: Puesto que no hay alguna regla en el lenguaje que nos diga qué tan distintas deben ser 2 variables entre sí, nada nos impide tener una variable suma y otra suam. Por lo tanto, sin conocimiento de lo que esperamos que el programa realice, no podemos deducir que es un error tipográfico y no una variable nueva distinta.
  3. Error no solucionable de manera sintáctica: Podemos identificar que la indentación es incorrecta, pero no tenemos una manera de decidir si la indentación debe ser tal que la instrucción print queda dentro o fuera del bloque for.
  4. Error solucionable sintácticamente: La computadora identifica que el resto de la línea corresponde a una cadena de texto, por lo que el desarrollador debe estar queriendo pasarla a la función print, y cierra el paréntesis al final de la línea.

Si lo meditamos un poco, la clasificación anterior se basó en qué tanta “ambigüedad” introdujo cada error. Y con eso damos pie al tema medular de esta discusión: la ambigüedad.

Las computadoras en esencia son simplemente máquinas que siguen instrucciones. Es común que al explicar a alguien por primera vez lo que es programar usemos la analogía de las recetas de cocina: Al programar utilizamos código para darle instrucciones a la computadora sobre lo que debe hacer, así como una receta de cocina le dice al cocinero los pasos para crear un platillo. El gran problema con la analogía anterior es que jamás he visto una receta de cocina que sea perfectamente específica. Términos como “fuego alto” o “al gusto” son completamente ambiguos, lo cuál no es necesariamente malo pues le dan la oportunidad al cocinero de expresarse y crear un platillo a su gusto. Pero cuando se trata de una computadora que va a ejecutar procesos exactos, no podemos permitirnos tales ambigüedades. No solo eso, un cocinero de manera deliberada puede alterar la receta y “arreglarla”, contrario a la computadora.

Las computadoras son las máquinas más tontas que existen, si no eres completamente preciso, nos van a dar un resultado incorrecto (en el mejor de los casos) o simplemente no van a funcionar.

Los lenguajes de programación suelen ser un poco menos tontos que las computadoras y pareciera factible incorporar algoritmos que identifiquen algunos errores tipográficos y los resuelvan.

Cuando hablamos de sintaxis de lenguajes de programación entendemos que tenemos una grámatica precisa que utiliza el compilador o intérprete del lenguaje para saber cuál es el propósito de cada comando que ingresamos. Si buscamos que nuestro compilador sea determinista (es decir, dado un mismo código siempre nos va a generar el mismo programa).

Supongamos que nuestro lenguaje de programación es suficientemente listo para resolver errores que realmente sólo se pueden resolver de una sola manera, es decir no hay ambigüedad. Además, entendemos por “resolver un error” que el compilador va a modificar nuestro código para generar algo válido dentro de nuestra gramática.

Planteo la siguiente pregunta. Si nuestro lenguaje de programación entiende estructuras sintácticas no ambiguas, ¿cuál es la diferencia con incorporar tal estructura en nuestra gramática?

Dicho de otra forma, si nuestro lenguaje sabe resolver ciertos errores y el programador a propósito explota esas capacidades del lenguaje, sabiendo que el resultado será el mismo, ¿no podemos decir que esa sintaxis “errónea” en realidad es sintaxis “correcta”?

Veamos un ejemplo utilizando el lenguaje JavaScript:

let suma = 0
for (let i = 0; i <= 100; i++) {
  suma += i
}

El código anterior implementa el mismo algoritmo que queríamos describir con Python.

Algo interesante es que el siguiente código es exactamente equivalente:

let suma = 0;
for (let i = 0; i <= 100; i++) {
  suma += i;
}

La única diferencia entre los dos es que el segundo utiliza ; al finalizar cada instrucción. Y por “exactamente equivalente” quiero decir que el intérprete del lenguaje va a aceptar ambos códigos y se va a comportar de la misma manera. ¿Qué nos dice esto?

JavaScript, en específico su sintaxis sobre los puntos y comas, es “inexacto” según nuestra definición anterior. Pero un programador del lenguaje probablemente no lo siente como un algoritmo inteligente para resolver errores tipográficos, sino que lo ve como una sintaxis válida del lenguaje.

Este podría ser un primer argumento en contra de los lenguajes imprecisos. No podemos tener un lenguaje impreciso si las partes imprecisas del lenguaje son gramáticas bien definidas. Por otro lado, si algo no está bien definido en el lenguaje, seguramente estamos cayendo en ambigüedades, y las computadoras no pueden trabajar con eso.

Este es un argumento parecido al que propone Douglas Hofstadter en su libro Gödel, Escher, Bach. Si un lenguaje de programación permite algo de flexibilidad en su sintaxis, caemos en alguno de los siguientes 3 casos:

  1. El programador es consciente de esos casos en donde hay flexibilidad y los utiliza de manera deliberada: Podemos decir que la “flexibilidad” se ha vuelto parte del lenguaje, como el ejemplo anterior de ; en JavaScript.
  2. El programador no es consciente de que el lenguaje permite tal flexibilidad: Tenemos un problema porque nuestro código ya no sirve para comunicar instrucciones precisas.
  3. El programador es consciente de la flexibilidad que permite el lenguaje, sin embargo, este sistema de corrección de errores es demasiado complejo para que el programador lo tenga presente todo el tiempo.

El caso interesante es el tercero, por lo siguiente. Si el sistema tiene alta flexibilidad de manera que el programador no tiene total conocimiento del algoritmo que implementa, esencialmente estamos tratando con inteligencia artificial. Hoy en día ese no es un concepto inconcebible: herramientas como GitHub Copilot y Kite buscan ser asistentes de programación que funcionan con inteligencia artificial.

Lo importante aquí es, ¿qué tan exactos son? En general podemos decir que si bien son herramientas útiles, no son perfectas. La evidencia empírica parece decirnos que no es posible tener herramientas que resuelvan los errores en nuestro código en todos los casos. Incluso aquellas herramientas populares como GitHub Copilot reportan fallas en el código que sugieren. Y tal vez no es justo utilizar este ejemplo por ser herramientas de recién creación y en constante mejora (discutiblemente toda la inteligencia artificial está en etapas muy tempranas). Por eso mismo, busquemos argumentos más sólidos.

Pensemos de otra forma. ¿Es posible que exista un ente perfecto de inteligencia artificial que no cometa errores? En el momento en el que entramos en el terreno de la inteligencia artificial, nuestras esperanzas de tener un lenguaje de programación inexacto o un asistente que repare nuestro código, deben dejar de existir. La inteligencia no es algo determinista y así como un humano (inteligente) tiene errores, lo mismo debemos esperar de cualquier sistema inteligente. La inteligencia está basada en probabilidades, y por lo mismo, siempre será posible cometer errores.

Ahora, antes dije que la única forma de tener un lenguaje de programación flexible pero útil es con inteligencia artificial, e incluso eso no es perfecto. La pregunta es, ¿cómo aseguro que no es posible crear un algoritmo capaz de siempre resolver nuestros errores de código?

En la década de 1930 tres grandes matemáticos dedicados a la lógica realizaron las contribuciones fundamentales que dieron lugar a la rama de las Ciencias Computacionales:

  • Kurt Gödel: Teoremas de incompletitud y funciones recursivas generales.
  • Alonzo Church: Cálculo Lambda.
  • Alan Turing: Máquinas de Turing.

Si bien me resulta fascinante el trabajo de estos 3 héroes del cómputo y podría estar horas hablando de ello, no me siento suficientemente conocedor como para escribirlo en este artículo y por ahora preferiría prescindir de la formalidad que conllevan.

Lo único que voy a mencionar por encima es uno de los resultados más importantes de Alan Turing: la indecibilidad de las máquinas de Turing.

La indecibilidad se refiere a la imposibilidad absoluta de crear un algoritmo que nos diga si una máquina de Turing alguna vez terminará su cómputo o no.

Por fortuna para aquellos que no conocen o no entienden perfectamente lo que es una máquina de Turing, tenemos una alternativa, gracias a la equivalencia entre máquinas de Turing y lenguajes de programación:

Es imposible crear un programa que, dado cualquier código de un lenguaje de programación, determine si ese programa alguna vez termina o se ejecuta infinitamente.

Podría no ser evidente cómo lo anterior influye en la imposibilidad de nuestro lenguaje de programación inexacto. Pero sí existe una traducción directa a nuestro problema.

Pensemos en nuestro compilador “inteligente” como un programa capaz de leer nuestro código, analizarlo y reparar los errores que tiene en una forma que es consistente con lo que nosotros pretendemos realizar como programadores. Entonces, un programador podría pretender escribir un programa que determine si un programa termina o no, y simplemente introducir algunos errores. Entonces, si le damos ese programa a nuestro compilador inteligente, este debe ser capaz de arreglar nuestro código y generar el programa que pretendíamos. Efectivamente hemos resuelto la indecibilidad de los lenguajes de programación. Pero, antes dijimos que la indecibilidad era una verdad matemática absoluta, entonces concluimos que el compilador que acabamos de describir no es algo posible.

Siento que lo anterior sería un perfecto final para este post, pero tampoco quiero dejarlo aquí de forma completamente pesimista. Si bien anteriormente dije que era imposible un compilador capaz de resolver todos nuestros errores, eso no significa que no existe un compilador capaz de resolver algunos de nuestros errores.

En efecto existen algunos errores para los cuáles podemos crear algoritmos que los identifiquen y reparen. De hecho, en distintas medidas, han existido y existen tales algoritmos incorporados en compiladores.

Sin embargo, los diseñadores de lenguajes han notado que en general no es buena idea intentar solucionar los errores del programador, tanto como es útil el señalar el error y ofrecer una posible solución.

Al encontrar errores y tratar de corregirlos en el compilador, no suele ser claro hasta qué punto somos capaces de ofrecer soluciones algorítmicas antes de cometer el error de cambiar la semántica del código. O peor aún, reparar un error que era síntoma de un error en la lógica del programador.

A pesar de ello, no todo está perdido. Probablemente uno de los contrastes más pronunciados en compiladores se puede ver al comparar el compilador de C y el compilador de Rust.

Si bien no pienso que un lenguaje sea mejor que otro, C tiene fama por sus mensajes de error comúnmente confusos y poco útiles para encontrar errores. Por otro lado, Rust ha calificado como el lenguaje con mejor experiencia de usuario. En gran parte esto se debe a los avances que se han hecho en el desarrollo de compiladores, al punto en que hoy es posible ofrecer sugerencias útiles a los desarrolladores para resolver los errores de compilación que presentan sus programas: sugerencias de puntuación, tipos incompatibles, errores en el alcance de variables, y muchos más.

Esta publicación ha sido una manera larga y compleja de decir que los compiladores no son nuestra salvación al momento de programar.

Me gustaría agregar que también existen muchas herramientas útiles para reducir la posibilidad de errores en nuestro código fuera del compilador: en general es de mayor utilidad un editor de texto configurado de manera que nos apoye en el proceso de desarrollo de software, antes que un compilador que haga sugerencias. Lo anterior toma especial relevancia si nos damos cuenta de que la gran mayoría de los errores de sintaxis son fácilmente prevenibles con un editor descente: correcta indentación; cierre automático de paréntesis, llaves, comillas, etc; errores de deletreo.

Creo que si algo hay rescatable de esta publicación sería, invierte tiempo en aprender el lenguaje, invierte tiempo en configurar tu entorno de desarrollo y no bases tu elección de lenguaje en la facilidad de sintaxis. Al final del día, las buenas prácticas y la familiaridad con la sintaxis se van construyendo.