Notxor tiene un blog

Defenestrando la vida

Introducción a PicoLisp

Notxor
2023-12-31

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 \(3 \cdot 10^3 \times 4 \cdot 10^3\) el resultado es \(12 \cdot 10^6\). Es decir, PicoLisp no tiene en cuenta automáticamente los decimales, sino que mantiene los enteros, pero escalados.

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:

jerarquita-tipos-picolisp.svg

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.

cell.svg

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:

bignum-picolisp.svg

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:

simbolo-minimo-picolisp.svg

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:

simbolo-ejemplo-picolisp.svg

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:

listas-picolisp.svg

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 forma lib: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.
Categoría: picolisp emacs

Comentarios

Debido a algunos ataques mailintencionados a través de la herramienta de comentarios, he decidido no proporcionar dicha opción en el Blog. Si alguien quiere comentar algo, me puede encontrar en esta cuenta de Mastodon, también en esta otra cuenta de Mastodon y en Diaspora con el nick de Notxor.

Si usas habitualmente XMPP (si no, te recomiendo que lo hagas), puedes encontrar también un pequeño grupo en el siguiente enlace: notxor-tiene-un-blog@salas.suchat.org

Disculpen las molestias.