Lingüística y lenguajes de programación.
Los lenguajes naturales (por ejemplo: el español o el inglés) tienden a ser ambiguos y en muchas ocasiones están sujetos a la interpretación, la cual puede variar en función del contexto, el interlocutor, el tono, etc.
Por otra parte los lenguajes de programación tienen el objetivo de describir procesos de computación o algoritmos a máquinas. Estos lenguajes tienen una descripción sintáctica y semántica precisa con el objetivo de evitar la ambigüedad propia de los lenguajes naturales.
Durante los años, el nivel de abstracción de los lenguajes de programación ha ido aumentando. Ésto ha proporcionado a los programadores herramientas con las que construir programas más complejos de forma más sencilla.
En este artículo veremos un ejemplo de cómo los lenguajes de programación pueden ser enriquecidos mediante ciertas características de los lenguajes naturales con el objetivo de crear nuevas abstracciones sobre las que trabajar.
¿Qué es una anáfora?
En lingüística, una anáfora es una expresión (normalmente un pronombre) cuyo significado depende de otra expresión previa. Las ánforas nos permiten hablar y mantener una conversación sobre algo sin necesidad de nombrarlo continuamente.
El diccionario de la Real Academia de la Lengua define la anáfora como:
f. Ling. Relación de identidad que se establece entre un elemento gramatical y una palabra o grupo de palabras nombrados antes en el discurso
Por ejemplo: en la frase «Coge el teléfono y quítalo de la mesa» el pronombre «lo» es una anáfora que hace referencia a «el teléfono».
¿Qué es una macro?
En programación, una macro (abreviatura de macroinstrucción) es una función que genera código. El proceso de generar código a partir de una macro se llama «macro expansión».
En los lenguajes de programación que soportan macros, hay dos diferencias fundamentales entre éstas y las funciones:
- Las funciones devuelven resultados, mientras que las macros se expanden y devuelven expresiones (estas expresiones pueden a su vez ser funciones que devuelvan resultados).
- La macro expansión tiene lugar en tiempo de compilación. En tiempo de ejecución sólo se ejecuta el código que haya generado la macro.
Las macros son un mecanismo de metaprogramación muy potente puesto que permiten añadir características y construcciones a un lenguaje definiéndolas en el propio lenguaje y sin ningún coste de rendimiento durante la ejecución del programa.
Un ejemplo de uso de macros se puede ver en el repositorio del lenguaje de programación Elixir, que está programado casi en su totalidad en el propio Elixir.
¿Qué es una macro anafórica?
El concepto de macro anafórica fue introducido por Paul Graham en su libro On Lisp, Advanced Techniques for Common Lisp. Una macro anafórica simula el concepto de anáfora de los lenguajes naturales en un lenguaje de programación.
Técnicamente podemos decir que una macro anafórica enriquece el ámbito de ejecución introduciendo en él automáticamente un nuevo identificador que toma el valor de una expresión ejecutada previamente.
Caso de uso y ejemplo de implementación.
A continuación veremos un ejemplo de la implementación y el funcionamiento de una macro anafórica. El lenguaje que usaremos para los ejemplos es Elixir.
Supongamos que necesitamos comprobar si una expresión devuelve un valor verdadero (en Elixir cualquier valor se considera verdadero excepto nil
y false
) y, en caso afirmativo hacer algo con él. Si la expresión a evaluar requiere un cálculo costoso, la memorizaríamos de la siguiente manera:
defmodule Universe do
def response_to_everything do
:timer.sleep(5000) # Simulate complex calculations for 5 seconds
42
end
def proclaim(data), do: "Whoa! The data is #{data}"
end
if result = Universe.response_to_everything do
IO.puts Universe.proclaim(result)
end
# Whooops! Bad things can happen here!
# The `result` variable is available outside of the `if` expression
IO.puts(result)
Este enfoque es muy simple pero define la variable result
en un ámbito superior al del if
donde resulta útil, lo que provoca que dicha variable siga siendo accesible posteriormente. Éste es un clásico problema de scoping que se resuelve creando un nuevo ámbito de vida para la variable result
:
defmodule Universe do
def response_to_everything do
:timer.sleep(5000) # Simulate complex calculations for 5 seconds
42
end
def proclaim(data), do: "Whoa! The data is #{data}"
end
# The `with` expression creates a new scope for the `result` variable
with result <- Universe.response_to_everything do
if result, do: IO.puts Universe.proclaim(result)
end
# Variable `result` is not available in this scope anymore
Este proceso de guardar el resultado y crear un nuevo scope donde esté disponible puede ser automatizado por medio de una macro.
defmodule Universe do
def response_to_everything do
:timer.sleep(5000) # Simulate complex calculations for 5 seconds
42
end
def proclaim(data), do: "Whoa! The data is #{data}"
end
defmodule Anaphora do
defp it, do: Macro.var(:it, nil)
defmacro if(expression, clauses) do
quote do
with unquote(it) <- unquote(expression) do
if unquote(it), unquote(clauses)
end
end
end
end
La macro quote
devuelve el AST que representa a las expresiones que contiene. La macro unquote
evalúa el valor real de la expresión que contiene antes de insertarlo en el AST. Podemos invocar nuestra macro así:
iex(1)> require Anaphora
Anaphora
iex(2)> Anaphora.if Universe.response_to_everything do
...(2)> # `it` variable is automatically created during macro expansion
...(2)> Universe.proclaim it
...(2)> end
"Whoa! The data is 42"
# 'it' variable doesn't exist outside of the macro
iex(3)> IO.puts(it)
** (CompileError) iex:3: undefined function it/0
A pesar de que la variable it
parece ser una variable libre, ésta ha sido creada durante el proceso de macro expansión de la macro Anaphora.if
. Como podemos ver, su comportamiento es similar al de una anáfora en el lenguaje natural, puesto que su valor hace referencia al de la expresión que hemos pasado como primer argumento.
Conclusión.
La diferencia entre el lenguaje natural y un lenguaje de programación es que mientras que el primero es ambiguo y está sujeto a la interpretación, el segundo tiene una especificación formal que pretende eliminar la ambigüedad.
Gracias a la metaprogramación podemos incluir o simular ciertas características de los lenguajes naturales que hagan nuestros programas más legibles y fáciles de entender.
A pesar de toda la flexibilidad que nos permite la metaprogramación y, en particular, las macros, es importante destacar que son un mecanismo potencialmente peligroso y del que no hay que abusar. El isomorfismo de los lenguajes como Lisp o Elixir hacen más sencillo este tipo de metaprogramación.
La macro anafórica que hemos implementado no es mas que un ejemplo muy simple. La librería anaphora contiene una implementación mucho más completa y robusta de varias macros anafóricas en Elixir. Su código es muy limpio y fácil de entender, si tienes curiosidad merece la pena echarle un vistazo.