Las Raices de Lisp

Share Embed Donate


Short Description

manual de lisp...

Description

Las Raíces de Lisp PAUL GRAHAM

 Borrador, Enero 18, 2002

En 1960, John McCarthy publicó un notable artículo en el que hizo por la programación algo  parecido a lo que Euclides Euclides hizo por la geometría. geometría. 1 Demostró cómo, dado un puñado de operadores simples y una notación para funciones, se puede construir un lenguaje de  programación. Llamó a este lenguaje Lisp, por "List Processing", Processing", debido a que una de sus  principales ideas era usar una estructura de datos simple llamada list tanto para el código como  para los datos. Vale la pena comprender lo que McCarthy descubrió, no sólo como un hito en la historia de las computadoras, sino como un modelo para lo que la programación tiende a convertirse en nuestro propio tiempo. Me parece que hasta la fecha ha habido dos modelos de programación limpios y consistentes: el modelo C y el modelo Lisp. Estos dos se asemejan a puntos elevados sobre el terreno, con tierras bajas pantanosas entre ellos. Conforme las computadoras se han vuelto más potentes, los nuevos lenguajes en desarrollo se han estado moviendo de manera continua hacia el modelo de Lisp. Una receta popular para los nuevos lenguajes de  programación en los últimos últimos 20 años ha sido la de tomar el modelo C de la informática informática y agregarle, poco a poco, partes tomadas del modelo de Lisp, tales como la escritura en tiempo de ejecución y la recolección de basura. En este artículo trataré de explicar en los términos más simples posibles lo que McCarthy descubrió. La idea no es sólo conocer el interesante resultado teórico descubierto hace cuarenta años, sino demostrar hacia dónde se dirigen los lenguajes. Lo inusual de Lisp —de hecho, la característica distintiva de Lisp— es que se puede escribir a sí mismo. Para comprender lo que McCarthy quiso decir con esto, volveremos sobre sus pasos, con su s u notación matemática traducida a código corriente de Common Lisp. 1 Siete Operadores Primarios Para empezar, definimos una expresión. Una expresión es ya sea un atom, que es una secuencia de letras (por ejemplo: foo) o una lista de cero o más expresiones, separadas por espacio en  blanco y encerradas por paréntesis. paréntesis. Estas son algunas expresiones: foo () (foo) (foo bar) (a b (c) d)

La ultima expresión es una lista de cuatro elementos, la tercera de las cuales es en si misma una lista de un elemento.  1

“Recursive Functions of Symbolic Expressions and Their Computation byMachine, Part I” Communications of the ACM  3:4, Abril 1960, pp. 184-195.

1

En aritmética la expresión 1+1 tiene un valor de 2. Las expresiones de Lisp validas también tienen valores. Si una expresión e arroja un valor  v decimos que e devuelve v. Nuestro siguiente  paso es definir qué tipos de expresiones puede haber, y qué valor devuelve cada una. Si una expresión es una lista, llamamos al primer elemento el operador y a los elementos restantes los argumentos. Vamos a definir siete operadores primarios (en el sentido de los axiomas): quote, atom, eq, car, cdr, cons, y cond. 1.

(quote  x) (quote  x)

devuelve x. Por facilidad de lectura abreviaremos como  ′x.

>(quote a) a > a a > (quote (a b c)) (a b c) ′

2.

devuelve el atom t si el valor de  x es un atom o la lista vacía. De lo contrario regresa (). En Lisp por lo general usamos el atom t para representar verdadero, y la lista vacía para representar falso.

(atom  x)

> (atom a) t >(atom (a b c)) () > (atom ()) t ′





Ahora que tenemos un operador cuyo argumento es evaluado podemos mostrar para qué sirve quote. Al poner  quote a una lista la protegemos de ser evaluada. Una lista sin quote que se da como argumento a un operador como atom es tratada como código: > (atom (atom t



a))

mientras que una lista a la que se le aplicó en este caso, una lista de dos elementos: > (atom ()

(atom



quote

es tratada como una simple lista,

a))



Esto es igual a la manera en que usamos las comillas en Inglés. Cambridge es un  pueblo en Massachusetts que tiene cerca de 90,000 personas. “Cambridge” es una  palabra que contiene nueve letras. 2

Quote puede parecer un concepto extraño, porque muy pocos otros lenguajes tienen algo parecido. Esta estrechamente ligada a una de las características más distintivas de Lisp: el código y los datos se componen de las mismas estructuras de datos, y el operador quote es la manera en que distinguimos entre ellos. 3.

devuelve t si los valores de  x y  y son el mismo atom o si ambos son la lista vacía, de otra manera devuelve ().

(eq  x y)

> (eq t > (eq () > (eq t

4.

a



a)

b)

()



(car  x)



())

espera que el valor de  x sea una lista, y devuelve su primer elemento ′

(a b c))

espera que el valor de  x sea una lista, y devuelve todo después del primer elemento.

(cdr  x)

> (cdr (b c)

6.





> (car a

5.

a





(a b c))

espera que el valor de  y sea una lista, y devuelve una lista que contiene el valor de  x seguido por los elementos del valor de  y.

(cons  x y)

> (cons 'a '(b c)) (a b c) > (cons 'a (cons 'b (cons 'c '()))) (a b c) > (car (cons 'a '(b c))) a > (cdr (cons 'a '(b c))) (b c)

7.

(cond ( p1 e1)...( pn en)) es evaluado como sigue. Las expresiones  p son evaluadas en orden hasta que una devuelve t. Cuando se encuentra una, el valor de

la correspondiente expresión e es devuelto como el valor de la totalidad de la expresión cond. > (cond ((eq 'a 'b) 'first) ((atom 'a) 'second)) second

3

En cinco de nuestros siete operadores primarios, los argumentos son siempre evaluados cuando una expresión que empieza con ese operador es evaluada. 2 Llamaremos a un operador de ese tipo una función. 2 Denotando Funciones Enseguida definimos una notación para describir funciones. Una función es expresada como (lambda ( p1... pn) e)donde  p ... pn son atoms (llamados  parámetros) y e es una expresión. A una expresión cuyo primer elemento es tal expresión 1

((lambda ( p1... pn) e) a1...an)

se le denomina llamada a función y su valor se calcula de la siguiente manera. Cada expresión ai es evaluada. Luego e es evaluada. Durante la evaluación de e, el valor de cualquier  ocurrencia de una de las  pi es el valor de la ai correspondiente en la más reciente llamada a función. > ((lambda (x) (cons x '(b))) 'a) (a b) > ((lambda (x y) (cons x (cdr y))) 'z '(a b c)) (z b c)

Si una expresión tiene un atom  f como su primer elemento que no sea uno de los operadores  primarios ( f ai...an)

y el valor de f es una función el valor de

(lambda ( p1... pn) e)

entonces el valor de la expresión es

((lambda ( p1... pn) e) a1...an)

En otras palabras, los parámetros pueden ser usados como operadores en expresiones lo mismo que como argumentos: > ((lambda (f) (f '(b c))) '(lambda (x) (cons 'a x))) (a b c)

Hay otra notación para funciones que habilita la función para referirse a si misma, dándonos  por lo tanto una manera conveniente para definir funciones recursivas. 3 

Las expresiones que empiezan con los otros dos operadores, quote y cond, son evaluados de manera diferente. Cuando una expresión quote es evaluada, su argumento no es evaluado, sino que sólo es devuelto como el valor total de la expresión quote . Y en una expresión cond valida, solo una ruta de subexpresiones en forma de L será evaluada. 3 Lógicamente, no necesitamos definir una nueva notación para esto. Podemos definir funciones recursivas en nuestra notación existente utilizando una función de las funciones denominada el combinador Y. Puede que McCarthy no supiera del combinador Y cuando escribió este documento; de cualquier manera, la notación label es más fácil de leer. 2

4

La notación (label  f  (lambda ( p1... pn) e))

denota una función que se comporta como (lambda ( p1... pn) e), con la propiedad adicional que una ocurrencia de  f dentro de e evaluara a la expresión label, como si  f fuera un  parámetro de la función. Supón que queremos definir una función (subst  x y z ), que toma una expresión  x, un atom  y, y una lista  z , y devuelve una lista como  z  pero con cada instancia de  y (a cualquier nivel de anidamiento) en  z reemplazada por  x. >(subst 'm 'b '(a b (a b c) d)) (a m (a m c) d)

Podemos denotar esta función como (label subst (lambda (x y z) (cond ((atom z) (cond ((eq z y) x) ('t z))) ('t (cons (subst x y (car z)) (subst x y (cdr z)))))))

Abreviaremos f =(label  f 

(lambda ( p1... pn) e))

como

(defun  f  ( p1... pn) e)

 por tanto (defun subst (x y z) (cond ((atom z) (cond ((eq z y) x) ('t z))) ('t (cons (subst x y (car z)) (subst x y (cdr z))))))

Incidentalmente, aquí vemos como obtener una cláusula por defecto en una expresión Una cláusula cuyo primer elemento es t siempre tendrá éxito. Así

cond.



(cond ( x y) ( t  z )) ′

es equivalente a lo que podríamos escribir en un lenguaje con sintaxis como if  x then  y else  z 

3 Algunas Funciones Ahora que tenemos una manera de expresar funciones, definimos unas nuevas en función de nuestros siete operadores primarios. Primero será conveniente introducir algunas abreviaciones 5

 para patrones comunes. Usaremos c xr, donde x es una secuencia de as o ds, como una abreviación para la correspondiente composición de car y cdr. Así por ejemplo (cadr e) es una abreviación para (car (cdr e)), que devuelve el segundo elemento de e. > (cadr '((a b) (c d) e)) (c d) > (caddr '((a b) (c d) e)) e > (cdar '((a b) (c d) e)) (b)

Asimismo, utilizaremos (list e1...en)  para (cons e1 ... (cons en



()) ... ) .

> (cons 'a (cons 'b (cons 'c '()))) (a b c) > (list 'a 'b 'c) (a b c)

Ahora definimos algunas funciones nuevas. Cambié los nombres de estas funciones agregando  puntos al final. Esto distingue las funciones primarias de aquellas definidas en función de estas, y también evita conflictos con funciones existentes de Common Lisp. 1.

(null.  x)

comprueba si su argumento es la lista vacía.

(defun null. (x) (eq x '())) > (null. 'a) () > (null. '()) t

2.

(and.  x  y)

devuelve t si ambos de sus argumentos lo hacen y de lo contrario

().

(defun and. (x y) (cond (x (cond (y 't) ('t '()))) ('t '()))) > (and. (atom 'a) (eq 'a 'a)) t > (and. (atom 'a) (eq 'a 'b)) ()

3.

(not.  x)

devuelve

t

si su argumento regresa

t.

6

(),

y ()si su argumento devuelve

(defun not. (x) (cond (x '()) ('t 't))) > (not. (eq 'a 'a)) () > (not. (eq 'a 'b)) t

4.

(append.  x y)toma dos listas y devuelve su

concatenación.

(defun append. (x y) (cond ((null. x) y) ('t (cons (car x) (append. (cdr x) y))))) > (append. '(a b) '(c d)) (a b c d) > (append. '() '(c d)) (c d)

5.

(pair.  x y)toma dos listas de la

misma longitud y devuelve una lista de listas de dos elementos que contienen pares sucesivos de un elemento de cada una.

(defun pair. (x y) (cond ((and. (null. x) (null. y)) '()) ((and. (not. (atom x)) (not. (atom y))) (cons (list (car x) (car y)) (pair. (cdr) (cdr y)))))) > (pair. '(x y z) '(a b c)) ((x a) (y b) (z c))

6.

toma un atom  x y una lista  y de la forma creada por  pair., y devuelve el segundo elemento de la primera lista en  y cuyo primer elemento es  x. (assoc.  x y)

(defun assoc. (x y) (cond ((eq (caar y) x) (cadar y)) ('t (assoc. x (cdr y))))) > (assoc. 'x '((x a) (y b))) a > (assoc. 'x '((x new) (x a) (y b))) new

7

4

La Sorpresa

Por tanto podemos definir funciones que concatenan listas, substituyen una expresión por otra, etc. Una notación elegante, tal vez, pero ¿y qué? Ahora viene la sorpresa. Resulta que también  podemos escribir una función que funciona como interprete para nuestro lenguaje: una función que toma como argumento cualquier expresión Lisp, y devuelve su valor. Aquí esta: (defun eval. (e a) (cond ((atom e) (assoc. e a)) ((atom (car e)) (cond ((eq (car e) 'quote) (cadr e)) ((eq (car e) 'atom)

(atom

(eval. (cadr e) a)))

((eq (car e) 'eq)

(eq

(eval. (cadr e) a) (eval. (caddr e) a)))

((eq (car e) 'car)

(car

(eval. (cadr e) a)))

((eq (car e) 'cdr)

(cdr

(eval. (cadr e) a)))

((eq (car e) 'cons)

(cons

(eval. (cadr e) a) (eval. (caddr e) a)))

((eq (car e) 'cond)

(evcon. (cdr e) a))

('t (eval. (cons (assoc. (car e) a) (cdr e)) a)))) ((eq (caar e) 'label) (eval. (cons (caddar e) (cdr e)) (cons (list (cadar e) (car e)) a))) ((eq (caar e) 'lambda) (eval. (caddar e) (append. (pair. (cadar e) (evlis. (cdr

e) a))

a))))) (defun evcon. (c a) (cond ((eval. (caar c) a) (eval. (cadar c) a)) ('t (evcon. (cdr c) a)))) (defun evlis. (m a) (cond ((null. m) '()) ('t (cons (eval.

(car m) a)

(evlis. (cdr m) a)))))

La definición de eval. es más larga que cualquiera de las otras que hemos visto antes. Analicemos como funciona cada parte. La función toma dos argumentos: e, la expresión a ser evaluada, y a, una lista que representa los valores que se le han dado a los atoms al aparecer como parámetros en las llamadas a función.

8

A esta lista se le llama el entorno y es del tipo creada por  pair.. Fue con la intención de construir y buscar estas listas que escribimos pair. y assoc. . La espina dorsal de eval. es una expresión cond con cuatro cláusulas. Cómo evaluamos una expresión depende de que tipo sea. La primera cláusula maneja atoms. Si e es un atom,  buscamos su valor en el entorno: > (eval. 'x '((x a) (y b))) a

La segunda cláusula de eval. es otra cond  para manejar expresiones de la forma (a …), donde a es un atom. Esta incluye todos los usos de los operadores primarios, y hay una cláusula para cada uno. > (eval. '(eq 'a

a) '())

t > (eval. '(cons x '(b c)) '((x a) (y b))) (a b c)

Todos estos (excepto

quote) llaman a eval.  para

encontrar el valor de los argumentos.

Las últimas dos cláusulas son más complicadas. Para evaluar una expresión cond llamamos a una función auxiliar denominada evcon., que se abre camino a través de las cláusulas recursivamente, buscando una en la que el primer elemento devuelva t. Cuando encuentra tal cláusula devuelve el valor del segundo elemento. > (eval. '(cond ((atom x) 'atom) ('t 'list)) '((x '(a b)))) list

La última parte de la segunda cláusula de eval. maneja llamadas a función que han sido  pasadas como parámetros. Funciona reemplazando al atom con su valor (que debe ser una expresión lambda o label) y evaluando la expresión resultante. Entonces (eval. '(f '(b c)) '((f (lambda (x) (cons 'a x)))))

se convierte a (eval. '((lambda (x) (cons 'a x)) '(b c)) '((f (lambda (x) (cons 'a x)))))

que devuelve (a

b c) .

Las últimas dos cláusulas en eval. manejan llamadas a función en las que el primer elemento es de hecho una expresión lambda o label. Una expresión label es evaluada al forzar  9

una lista del nombre de la función y la función en si misma al entorno, y llamando luego a eval . en una expresión con la expresión interior  lambda sustituida por la expresión label. Es decir, (eval. '((label firstatom (lambda (x) (cond ((atom x) x) ('t (firstatom (car x)))))) y) '((y ((a b) (c d)))))

se convierte en (eval. '((lambda (x) (cond ((atom x) x) ('t (firstatom (car x))))) y) '((firstatom (label firstatom (lambda (x) (cond ((atom x) x) ('t (firstatom (car x))))))) (y ((a b) (c d)))))

que eventualmente devuelve a. Finalmente, una expresión de la forma ((lambda ( p1... pn) e) a1...an) es evaluada llamando en primer lugar  evlis.  para obtener una lista de valores (v1 ... vn) de los argumentos a1...an, y evaluando luego e con ( p1 v1) ... ( pn vn) añadido al frente del entorno. Entonces (eval. '((lambda (x y) (cons x (cdr y))) 'a '(b c d)) '())

se convierte en (eval. '(cons x (cdr y)) '((x a) (y (b c d))))

que eventualmente devuelve

(a c d) .

5 Repercusiones Ahora que entendemos como funciona eval, demos un paso atrás y consideremos lo que significa. Lo que tenemos aquí es un extraordinariamente elegante modelo de computación. Utilizando sólo quote, atom, eq, car, cdr, cons, y cond,  podemos definir una función, eval., que en realidad implementa nuestro lenguaje, y luego utilizando eso podemos definir cualquier  función adicional que queramos.

10

Ya había modelos de computación, por supuesto ― el más notable la Maquina de Turing. Pero leer los programas de la Maquina de Turing no es muy edificante. Si quieres un lenguaje para describir algoritmos, puede que desees algo más abstracto, y esa fue una de las metas de McCarthy al definir Lisp. El lenguaje que definió en 1960 carecía de mucho. No tiene efectos secundarios, no tiene ejecución secuencial (que de cualquier manera sólo es útil con efectos secundarios), no tiene números prácticos 4 ni ámbito dinámico. Pero sorprendentemente estas limitaciones pueden ser  remediadas con muy poco código adicional. Steele y Sussman muestran como hacerlo en un famoso articulo llamado The Art of the Interpreter. 5 Si comprendes el eval de McCarthy, comprendes más que sólo una etapa en la historia de los lenguajes. Estas ideas son todavia el núcleo semántico de Lisp hoy en día. Así que estudiar el articulo original de McCarthy nos muestra, en cierto sentido, lo que Lisp es realmente. No es tanto algo que McCarthy diseñara como algo que descubrió. No es intrínsecamente un lenguaje  para IA o prototipado rápido, o cualquier otra tarea de ese nivel. Es lo que obtienes (o una cosa que obtienes) cuando tratas de axiomatizar la computación. Con el tiempo, el lenguaje promedio, queriendo decir con esto el lenguaje utilizado por el  programador promedio, ha crecido consistentemente hasta acercarse a Lisp. Así que al comprender eval estas comprendiendo lo que probablemente será el principal modelo de computación en el futuro.

 4

Es posible hacer aritmética en el Lisp de 1960 de McCarthy utilizando por ejemplo una lista de n atoms para representar el numero n. 5 Guy Lewis Steele, Jr. y Gerald Jay Sussman, The Art of the Interpreter , o the Modularity Complex (Partes Cero, Una y Dos), MIT AI Lab Memo 453, Mayo 1978.

11

Notas Al traducir las notaciones de McCarthy a código funcional he tratado de cambiar lo menos  posible. Estuve tentado a hacer el código más fácil de leer, pero quise conservar el sabor del original. En el articulo de McCarthy falso es representado por  f, no por la lista vacía. Utilice () para representar falso para que los ejemplos funcionaran en Common Lisp. En ningún lugar el código depende en que ocurra falso para que también sea la lista vacía; nada esta nunca en contra sobre el resultado devuelto por un predicado. Me salté el construir listas a partir de pares de puntos, porque no los necesitas para comprender  eval. También me salté mencionar  apply, aunque fue apply (una forma muy temprana de ella, cuyo propósito principal era aplicar comillas a los argumentos) lo que McCarthy llamo en 1960 la función universal; eval era entonces solo una subrutina que apply llamaba para realizar todo el trabajo. Definí a list y los c xrs como abreviaciones porque así es como McCarthy lo hizo. De hecho los c xrs podían haber sido todos definidos como funciones ordinarias. Igual list si modificábamos eval, lo que podríamos hacer fácilmente, para permitir que las funciones tomaran cualquier cantidad de argumentos. El articulo de McCarthy sólo tenia cinco operadores primarios. Utilizó cond y quote  pero  pudo haber pensado en ellos como parte de su metalenguaje. De igual forma no definió los operadores lógicos and y not, pero esto no es tanto problema porque las versiones adecuadas  pueden ser definidas como funciones. Al definir  eval. llamamos otras funciones como pair. y assoc., pero cualquier llamada a alguna de las funciones que definimos en función de los operadores primarios puede ser  reemplazada por una llamada a eval.. Es decir, (assoc. (car e) a)

 pudo haber sido escrita como (eval. '((label assoc. (lambda (x y) (cond ((eq (caar y) x) (cadar y)) ('t (assoc. x (cdr y)))))) (car e) a) (cons (list 'e e) (cons (list 'a a) a)))

Había un pequeño error en el eval. de McCarthy. La línea 16 era (equivalente a) (evlis. (cdr e) a) en lugar de sólo (cdr e), lo que provocaba que los argumentos en una llamada a función nombrada fuera evaluada dos veces. Lo cual sugiere que esta 12

descripción de eval. todavía no había sido implementada en el lenguaje de maquina IBM 704 cuando el articulo fue enviado. También demuestra lo difícil que es estar seguro de la longitud correcta de cualquier programa sin ejecutarlo primero. Encontré otro problema en el código de McCarthy. Después de dar la definición de eval  pasa a dar algunos ejemplos de funciones de orden superior  ―funciones que toman otras funciones como argumentos. Él define maplist: (label maplist (lambda (x f) (cond ((null x) '()) ('t (cons (f x) (maplist (cdr x) f))))))

luego lo utiliza para escribir una sencilla función diff  para diferenciación simbólica. Pero diff  pasa a maplist una función que utiliza x como un parámetro, y la referencia a este es capturada por  el parámetro x dentro de maplist.6 Es un elocuente testimonio a los peligros del ámbito dinámico que incluso ya el primer ejemplo de funciones Lisp de orden superior estaba roto debido a ello. Puede ser que McCarthy no estuviera completamente consciente de las implicaciones del ámbito dinámico en 1960. El ámbito dinámico  permaneció en implementaciones Lisp por un periodo sorprendentemente largo―hasta que Sussman y Steele desarrollaron Scheme en 1975. El ámbito léxico no complica mucho la definición de eval, pero  puede hacer más difícil escribir compiladores.



Los programadores Lisp de hoy día usarían mapcar aquí en lugar de maplist. Este ejemplo clarifica un misterio: porque maplist esta en Lisp para empezar. Era la función de mapeo original, y mapcar una adición posterior. 6

Traducido de The Roots of Lisp por Paul Graham. Traducción: Armando Alvarez con la colaboración de Alonso Soto y Hanedi Salas

13

View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF