Introducción a PicoLisp
Llevo unas semanas trasteando ya con PicoLisp y he querido poner por escrito las primeras impresiones este final de año. Como propósito del año nuevo, ya comenté que la idea es hacer algún proyecto para aprender cómo funciona y estas son las primeras impresiones.
He de confesar que se me está atragantando un poco, quizá por el ansia viva de intentar tragar más de lo que puedo masticar. Quiero decir, que me las daba de esto está controlao y no, no lo está. Después de un par de guantazos propinados por el sistema para despertarme de mi error, tuve que volver a la documentación, tratar de poner en orden mis ideas, olvidar algunos prejuicios, formados por el conocimiento previo de Lisp y Scheme, y mejorar la concentración en lo que estoy haciendo.
Torpe de mí, pensé que siendo un Lisp la cosa iba a ser fácil, porque muchos conceptos los tengo ya asumidos e interiorizados. El problema es que no es un common lisp, ni tampoco un Scheme. Mantiene la sintaxis de la familia, pero es otra cosa completamente distinta.
En teoría, PicoLisp es una herramienta que proporciona todo lo que, generalmente, se necesita para hacer una aplicación, pero de manera minimalista, para no tomar excesivas decisiones de diseño. Sin embargo, sí hay muchas decisiones tomadas, que pueden ser vistas como limitaciones, pero a la vez proporciona una flexibilidad extraordinaria que permite hacer cualquier cosa.
Por ejemplo, en 28 líneas de script tienes un mínimo ejemplo de chat funcional, multihilo.
#!/usr/bin/picolisp /usr/lib/picolisp/lib.l (de chat Lst (out *Sock (mapc prin Lst) (prinl) ) ) (setq *Port (port 4004)) (loop (setq *Sock (listen *Port)) (NIL (fork) (close *Port)) (close *Sock) ) (out *Sock (prin "Please enter your name: ") (flush) ) (in *Sock (setq *Name (line T))) (tell 'chat "+++ " *Name " arrived +++") (task *Sock (in @ (ifn (eof) (tell 'chat *Name "> " (line T)) (tell 'chat "--- " *Name " left ---") (bye) ) ) )
¿Interesante? Pues vamos a ver cómo trabajar en Emacs con él. Luego ya me meteré en los aspectos más desconcertantes para mí.
Configurar Emacs para PicoLisp
Seré breve. Encontré un paquete en Melpa que se llama plisp-mode
y
otro para autocompletado company-plisp
. Los instalé los dos
siguiendo sus correspondientes documentaciones. El código es sencillo:
(use-package plisp-mode :defer t :init (require 'inferior-plisp) :custom (plisp-documentation-unavailable t)) (use-package company-plisp :defer t :after company) (add-to-list 'auto-mode-alist '("\\.l$" . plisp-mode))
Hay que tener en cuenta las siguientes aclaraciones. Tengo la
instalación de PicoLisp de manera local. El paquete plisp-mode
busca la documentación de manera global así que me aparecía
constantemente un error por no encontrarla y desactivé la búsqueda de
la misma activando la variable plisp-documentation-unavailable
.
También hice otras pruebas creando los enlaces necesarios para
convertir la instalación local en global y seguía sin encontrar la
documentación. Sin embargo, en la línea de comandos la documentación
está disponible cuando inicias PicoLisp en modo debug.
Para leer la documentación desde línea de comandos, por tanto, hay que
lanzar el REPL en modo depuración. El sistema leerá los html
que
componen la documentación utilizando el navegador w3m
. Por ejemplo,
llamando a la función (doc 'de)
, nos mostrará el contenido de la
documentación de dicha función, llamando a dicho navegador de modo
texto. w3m
es, por tanto, una dependencia si quieres acceder a la
documentación desde pil
.
Por último, la convención dice que el código de PicoLisp se
encuentra en archivos con extensión .l
y añado dicha extensión,
asociándola a plisp-mode
en la lista de modos.
Choques para programadores Lisp
Es evidente la sintaxis de tipo Lisp con sus paréntesis en danza,
pero para un programador de este tipo de lenguajes tiene pocas más
semejanzas y hacerse una idea de lo que hace le obliga a utilizar su
imaginación. Algunas funciones parecen sonar setq
, loop
... pero
de
en lugar de define
, que admite una lista de parámetros con el
formato Lst
en lugar del habitual (...)
. Variables que comienzan
con *
, como *Name
, mientras que otras no, como Lst
. Es decir,
hay un montón de idiosincrasias que hay que tener en cuenta.
La primera bofetada fue por su forma de establecer las variables. Lo
de utilizar la primera letra en mayúsculas, por convención, pues
tampoco es que me llame mucho la atención, o utilizar *
para señalar
las variables globales mediante un prefijo simple. Pero quizá algún
programador de Lisp o Scheme entenderá mejor lo que quiero decir
con este código:
: (setq A '(Este es su valor)) -> (Este es su valor) : (put 'A 'clave1 'valor1) -> valor1 : (put 'A 'clave2 'valor2) -> valor2 : (show 'A) A (Este es su valor) clave2 valor2 clave1 valor1 -> A
Sí, se pueden meter pares clave-valor como parte de cualquier
«variable». Se introducen con put
y se extraen con get
. Más
adelante veremos que tiene que ver con cómo se definen los símbolos
dentro de la máquina virtual. Pero de primeras me sonó marciano.
Se puede apreciar que la definición de variables se utiliza setq
,
también las podemos definir con de
, aunque con su propia
idiosincrasia:
: (de ho "hola") -> ho : ho -> ("hola") : (ho) -> NIL : (de ho . "hola") # ho redefined -> ho : ho -> "hola"
La primera versión de ho
, en ese código, puede no ser la deseada.
De momento me ciño a utilizar setq
para las variables y de
para
las funciones, para no liarme.
Luego me llevé otra bofetada aún más gorda con los números. Leí, así
de pasada, que soportaba sólo operaciones de coma fija, por lo que
hay que especificar el número de decimales cuando los necesitas.
Bueno, no es algo extraño y ya lo había vivido en otro tipo de
sistemas, como la calculadora de línea de comandos bc
, pero el
choque con la realidad fue un poco más traumático.
? (+ (* 3 4) 1) -> 13 ? : (/ @ 3) -> 4 : (scl 3) -> 3 : (+ (* 3.0 4.0) 1) -> 12000001
¿Cómo? ¿Qué está ocurriendo? Pues sí, a los números hay que darles un poco más de cariño.
La cantidad de decimales se almacena en una variable global que se
llama *Scl
y podemos establecerlos con la función scl
, como se
aprecia en el código anterior. Pero se puede ver que internamente
3.0
se convierte en 3000
y 4.0
en 4000
, al establecerse tres
posiciones decimales. Y, por esto mismo, al multiplicar los números el
resultado es 12.000.000
, o expresado de otro modo: al multiplicar
Con las sumas y restas no hay problema, porque los números cuando
tienen una escala, se mantienen dentro de la escala, pero en las
multiplicaciones y divisiones hay que compensar la escala. Para ello
se utiliza la función */
, que recibe tres parámetros numéricos y
multiplica los dos primeros para luego dividir el resultado por el
tercero. Por lo tanto, se utiliza la multiplicación o división por
1.0
para mantener la escala en orden:
: (+ (*/ 3.0 4.0 1.0) 1.0) -> 13000 : (*/ 1.0 13.0 4.0) -> 3250 : (format (*/ 1.0 13.0 4.0)) -> "3250" : (format (*/ 1.0 13.0 4.0) *Scl) -> "3.250" : (format "3.25" *Scl) -> 3250 :
Después, se puede contar con la función format
para convertir
números en cadenas y cadenas en números.
Al principio, toda esta movida me parecía un sindiós. Luego, me puse a mirar cómo funciona el chismático y encontré los porqués y las explicaciones a todas estas idiosincrasias. Aunque estuvieron a punto de echarme para atrás.
Tipos de datos
Puesto que las primeras aproximaciones fueron un poco frustrantes, lo siguiente que hice es echar un vistazo a la documentación buscando por qué estaba pasando lo que pasaba. Os hago un resumen de lo que encontré y cómo funciona la máquina virtual de PicoLisp, que soporta los siguientes tipos de datos:
En base a la definición básica de cell
, soporta:
- Los tres tipos de datos básicos: Números, símbolos y listas.
- Los tres tipos de símbolos: Internal, Transient y External.
- El símbolo especial
NIL
.
Un tipo de dato cell
se define, como se ve en la siguiente imagen,
en un conjunto de CAR
y CDR
. Algo que es común en los lenguajes
Lisp.
Los números
Hay dos tipos de números, los cortos y los largos. Ambos serán siempre enteros. Un número corto cabe en 60 bits, e internamente, tiene la forma:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxS010
donde las x
representan el valor numérico. Se pone el bit 1 a 1 para
señalar que es un número corto y S
representa el signo. Si en lugar
de tener marcado el bit 1 tiene marcado el bit dos, es decir:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxS100
estaremos ante un número largo y las x
se interpretan como un
puntero al siguiente dígito:
Es decir un bignum
se convierte en una lista de dígitos, marcada
internamente como número largo.
Los símbolos
Un símbolo es más complejo que un número. Cada símbolo tiene un valor
y opcionalmente tendrá un nombre y un número arbitrario de
propiedades. El más sencillo sería un símbolo sin nombre ni
propiedades, por lo que podría contenerse en una sola cell
:
Internamente, para indicar que es un símbolo se activará el bit 3, de la siguiente forma:
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx1000
Por ejemplo, si tenemos definido un símbolo con el nombre
"abcdefghijklmno"
que tenga tres propiedades además de su
correspondiente valor, podríamos tener el siguiente esquema:
Una propiedad es un par clave-valor, si sólo aparece una clave se
toma el valor de dicha propiedad como un T
booleano.
Las listas
Mientras los números y los símbolos se consideran atom
dentro de
PicoLisp, las listas son consideradas un tipo complejo a partir del
cual se pueden implementar otras estructuras como arrays, colas, pilas
o árboles.
Normalmente el CDR
de cada célula apunta a la célula siguiente:
La última célula de una lista puede tener un valor NIL
o apuntar a
un atom
. En este último caso se le llama dotted pair, pues la
notación en texto es marcarlo con un punto de separación, como se hace
en el resto de sus primos Lisp y Scheme. Dejo para el lector el
tema de las listas circulares, que también se pueden generar llamando
a la función circ
, pero que son un caso especial que hay que manejar
con cuidado pues podemos generar bucles infinitos en funciones que las
recorran y se utilizan con menos frecuencia.
Podemos tener listas de un sólo elemento. Por ejemplo (A)
es una
lista que tendrá una sola célula con el carácter A
en el CAR
y
NIL
en el CDR
.
Tipos de símbolos
PicoLisp es case sensitive, no como CLisp. Es decir, los
símbolos Dato y dato no son el mismo. Además, por convención, las
variables utilizan la primera letra en mayúscula, mientras que las
funciones son todo minúsculas. Esto evita también el choque de nombres
y pueden existir en el mismo sistema una variable Foo
y una función
foo
.
Además se utilizan las siguientes convenciones para los nombres de los símbolos:
- Las variables globales comienzan con un asterisco, por ejemplo:
*Foo
. - Las constantes globales se escriben en mayúsculas todo:
FOO
- Las funciones y otros símbolos globales comienzan con minúsculas:
foo
. - Los símbolos locales comienzan con la primera letra mayúscula:
Foo
. - Las funciones locales comienzan con un carácter de subrayado:
_foo
. - Las clases comienzan con un signo
+
:+Foo
o+foo
.- en minúscula para marcar clases abstractas,
- en mayúscula para el resto de clases.
- Los métodos terminan con un símbolo
>
:foo>
. - Las variables de clase se indican también con la primera letra
mayúscula, como las variables locales:
Foo
.
Estas convenciones son voluntarias, pero deseables. Sin embargo, es obligatorio identificar algunos casos, como el tipo de símbolo:
- Los símbolos temporales o transient se marcan con el símbolo de
dobles comillas
"
. - Los símbolos externos se marcan utilizando el par
{...}
. Los patrones se marcan con un carácter
@
:: (match '(@A es @B) '(Esto es una prueba)) -> T : @A -> (Esto) : @B -> (una prueba) :
- Los símbolos provenientes de librerías contienen un sigo
:
, con la formalib:simb
.
Hay tres tipos fundamentales de símbolos, además del símbolo especial
NIL
:
- Internal
- Los símbolos internal son símbolos normales, que
pueden ser definiciones de variables o funciones. Se indexan en una
estructura y por lo tanto no puede haber dos símbolos con el mismo
nombre. El valor inicial de un símbolo de este tipo, cuando se crea
es
NIL
. - Transient
- Los símbolos transitorios se guardan temporalmente en su propio índice, —como cuando se lee el código fuente desde un archivo— y se liberan después.
- External
- Los símbolos externos residen en un fichero de base de datos o en otros recursos que se cargan en memoria. Los símbolos externos se mantienen en un índice y mientras están cargados en memoria no puede haber dos con el mismo nombre. Además tienen codificada su dirección externa junto al nombre, para poder guardar los cambios.
NIL
- Este símbolo es especial, sólo existe uno en todo el
sistema de PicoLisp y, además de señalar el final de las listas y
ser el inicio de la jerarquía de clases, se puede utilizar como
valor booleano falso, o como cadena de largo cero, o como valor
NaN
, final de fichero, etc.
El código
El código se guarda en símbolos internos. Una definición de una función lo que hace es crear un símbolo cuyo valor es el código. No hay, por tanto, diferencia entre el tratamiento del código y de los datos, son equivalentes.
Para definir una función se utiliza normalmente una llamada a la
función de
:
: (de hola () (prinl "¡Hola Mundo!")) -> hola : (hola) ¡Hola Mundo! -> "¡Hola Mundo!" :
Pero el resultado sería idéntico si utilizamos setq
:
: (setq hola '(() (prinl "¡Hola Mundo!"))) -> (NIL (prinl "¡Hola Mundo!")) : (hola) ¡Hola Mundo! -> "¡Hola Mundo!" :
Otro de los problemas, que me encuentro frecuentemente al escribir
código, es el interpretar el '
como en Lisp o en Scheme. En
PicoLisp el quote
se aplica a toda la lista, no sólo al primer
elemento. Esto me desconcertó un poco al principio y aún no me he
acostumbrado del todo. Aunque también he aprendido que se puede forzar
la evaluación de alguna expresión interna utilizando el carácter ~
.
: '(1 (+ 2 3) 4) -> (1 (+ 2 3) 4) : '(1 ~(+ 2 3) 4) -> (1 5 4) :
No hay macros explícitas, porque cualquier función puede devolver una cadena de texto, que si mantiene la sintaxis de PicoLisp, está generando código como lo haría una macro. Si vas a utilizar metaprogramación es conveniente que le eches un ojo antes a cómo evalúa el código la máquina virtual de PicoLisp.
Tampoco hay una expresión lambda
, pues, como hemos visto al utilizar
una función setq
para definir una lambda, se puede generar una
función anónima simplemente con quote
:
(setq hola (quote () (prinl "¡Hola Mundo!")))
O por ejemplo:
: ('((X) (* X X)) 3) -> 9 : ((quote (X) (* X X)) 9) -> 81 : (setq f '((X) (* X X) ) ) -> ((X) (* X X)) : (f 3) -> 9 :
Si te molestan mucho los paréntesis los puedes agrupar utilizando [
y ]
. Por ejemplo, la definición anterior la podríamos haber definido
también como:
: (setq f '((X) (* X X] -> ((X) (* X X)) : (f 3) -> 9
Funciones con un número indeterminado de parámetros
Para las ocasiones, en que no sabemos cuántos parámetros puede recibir
una función se utiliza el comodín @
y se utilizan las variables
args
, next
y rest
para evaluar elemento a elemento la lista de
parámetros:
: (de foo @ (while (args) (println (next) (args) (rest)) ) ) -> foo : (foo 1 2 3 4) 1 T (2 3 4) 2 T (3 4) 3 T (4) 4 NIL NIL -> NIL :
En el código anterior:
args
es un booleano que indica si hay más argumentos que evaluar.next
devuelve el siguiente argumento a evaluar.rest
devuelve una lista con los argumentos que faltan por evaluar.
Conclusiones
Estos son los ladrillos básicos de PicoLisp. Algunos me ha costado cierto trabajo digerirlos, sin embargo, ha sido más por tener los procesos mentales acostumbrados a otro tipo de sistemas. Aún cometo muchos errores cuando escribo código, precisamente por esto. Aunque comencé estrellándome, confiado en mi conocimiento de Common Lisp y de Scheme, al mirar cómo está hecha la máquina virtual y «los ladrillos» de este sistema, me fue más fácil interiorizar cómo funcionan la cosas y cometer menos errores.
A partir de estos chismáticos básicos, PicoLisp proporciona algunas características interesantes, que dejo para futuros artículos:
- Soporte nativo de Programación Orientada a Objetos.
- Base de Datos orientada a almacenar objetos POO.
- Lenguaje de consulta de la BD basado en Prolog.
- GUI basado en
html
y su correspondiente servidor de aplicaciones. - Sistema de debug incorporado
- Editor de código interno basado en
vi
.
Comentarios