LISP: los programas como una manera de estructurar el pensamiento

LISP: los programas como una manera de estructurar el pensamiento

LISP apareció a finales de los años 50 alrededor del trabajo de John McCarthy sobre computación simbólica. Su intención original no era simplemente crear otro lenguaje para hacer cálculos numéricos: McCarthy estaba interesado en manipular expresiones que pudieran representar tanto información como instrucciones, dentro de su proyecto del Advice Taker. Por eso una de las ideas fundacionales de Lisp fue trabajar con expresiones simbólicas y estructuras de listas.

Y aquí aparece algo que, visto desde lenguajes modernos, puede resultar extraño.

En Lisp, una expresión como esta:

(+ (* 2 3) 4)

no es solamente texto que el compilador debe interpretar. Después de ser leída, puede representarse como una estructura de datos equivalente a un árbol:

+
├── *
│   ├── 2
│   └── 3
└── 4

La sintaxis de Lisp está construida de una forma tan regular que la estructura del programa queda muy cerca de una estructura de datos ordinaria. Esto es lo que suele resumirse con la idea de que el código puede manipularse como datos. Conviene no tomar la frase literalmente: no toda estructura de datos es código, pero una expresión Lisp puede representarse como datos Lisp y posteriormente evaluarse.

Por ejemplo:

(defparameter *expr*
  '(+ (* 2 3) 4))

*expr*
;; => (+ (* 2 3) 4)

(eval *expr*)
;; => 10

El ' (abreviatura de quote) es importante: evita que la expresión se evalúe inmediatamente y permite tratarla como una estructura. eval, posteriormente, puede evaluar esa estructura. En Common Lisp esto está definido explícitamente en el modelo del lenguaje.

Esto cambia ligeramente la pregunta que uno se hace al programar.

En muchos lenguajes pensamos primero en qué instrucciones escribir. Con Lisp es bastante natural preguntarse primero:

> ¿Qué estructura representa el problema que estoy intentando expresar?

Por ejemplo, si queremos representar una operación matemática, podemos verla como un árbol. Si queremos representar una regla, una consulta o una transformación, podemos construir estructuras similares. No es casualidad que los primeros trabajos sobre Lisp estuvieran ligados a la manipulación simbólica y a la inteligencia artificial.

La consecuencia más interesante aparece con las macros.

Una macro no es simplemente una función que devuelve un valor. Puede tomar una expresión Lisp y producir otra expresión Lisp que será evaluada después. Common Lisp define precisamente la expansión de macros como la transformación de una forma en otra.

Un ejemplo pequeño:

(defmacro when-not (condition &body body)
  `(unless ,condition
     ,@body))

Ahora podemos escribir:

(when-not listo
  (print "Todavía no"))

y la macro puede transformarlo conceptualmente en:

(unless listo
  (print "Todavía no"))

Es decir, el programador no solamente está escribiendo instrucciones: también puede definir nuevas formas de expresar instrucciones. En ese sentido, el lenguaje se convierte parcialmente en algo que el propio programador puede moldear. El proceso de expansión de macros está documentado tanto en Common Lisp como en los sistemas de macros de Scheme.

Esta es probablemente la parte menos obvia de Lisp.

Su ventaja histórica no está simplemente en que las listas sean cómodas, sino en que existe una continuidad bastante pequeña entre pensar una estructura, escribirla, manipularla y convertirla en comportamiento. Esa cercanía explica por qué cursos como Structure and Interpretation of Computer Programs utilizan Scheme —un dialecto de Lisp— para enseñar abstracción estructural, procedimental y metalingüística, en lugar de limitarse a enseñar sintaxis.

Quizá por eso la frase “los programas son una manera de estructurar el pensamiento” encaja tan bien como una interpretación de Lisp. No porque Lisp obligue a pensar de una única manera, sino porque hace especialmente visible algo que otros lenguajes pueden esconder: programar también consiste en decidir qué estructuras mentales queremos convertir en estructuras manipulables por una máquina.

Los siete operadores primitivos

En su artículo Recursive Functions of Symbolic Expressions and Their Computation by Machine (1960), John McCarthy definió un pequeño conjunto de funciones primitivas con las que es posible construir casi todo el resto del lenguaje: quote, atom, eq, car, cdr, cons y cond.

quote

Evita que una expresión sea evaluada y la devuelve tal cual como estructura de datos.

(quote (+ 1 2))
;; => (+ 1 2)

'hola
;; => HOLA

La abreviatura ' es azúcar sintáctico para quote. Sin quote, el símbolo hola sería interpretado como una variable; con quote se lo trata como un dato literal.

atom

Devuelve t (verdadero) si el argumento es un átomo y nil si es una lista. Un átomo es un valor indivisible, como un número o un símbolo.

(atom 5)
;; => T

(atom 'manzana)
;; => T

(atom '(1 2 3))
;; => NIL

eq

Compara dos átomos y devuelve t si son el mismo.

(eq 'a 'a)
;; => T

(eq 'a 'b)
;; => NIL

Solo compara átomos; para comparar listas hace falta una función definida a partir de estas primitivas.

car

Devuelve el primer elemento de una lista.

(car '(1 2 3))
;; => 1

(car '((a b) c))
;; => (A B)

cdr

Devuelve el resto de una lista, es decir, la lista completa sin su primer elemento.

(cdr '(1 2 3))
;; => (2 3)

(cdr '((a b) c))
;; => (C)

cons

Construye una nueva lista agregando un elemento al frente de otra lista.

(cons 0 '(1 2 3))
;; => (0 1 2 3)

(cons '(a b) '(c d))
;; => ((A B) C D)

cond

Evalúa condiciones en cadena y devuelve el resultado de la primera cuyo test sea verdadero. El último (t ...) funciona como un else.

(cond
  ((eq 'x 'a) 1)
  ((eq 'x 'x) 2)
  (t 3))
;; => 2

Estas siete operaciones permiten representar recursividad, construir y recorrer listas, comparar valores y decidir flujos; el resto de Lisp puede definirse conceptualmente a partir de ellas.