Notxor tiene un blog

Defenestrando la vida

Introducción a Chicken Scheme

Notxor
2023-09-07

En este artículo quiero contar de manera rápida y sin entrar en demasiado detalle, cómo estoy utilizando Chicken Scheme y por qué me gusta tanto, por encima de otros Scheme más populares, o incluso por delante de Common Lisp. Lo haré contando paso a paso un ejemplo de programación sencillo, nada complicado de entender o con algoritmos complejos. Desde los primeros pasos en la línea de REPL hasta la compilación en un ejecutable independiente.

Sin embargo, esto no es un curso de Scheme, ni de programación. Entiendo, desde el principio, que quien lea esto está interesado en el tema de la programación y sabe algo sobre el tema. Encontrarás, por tanto, que muchas cosas las daré por sabidas. También supongo que sabes cómo instalar el Chicken Scheme y cómo configurar tu editor favorito para trabajar con Scheme.

El intérprete se puede utilizar también para programar pequeños scripts para ejecutarlos sin necesidad de compilación. Por ejemplo, para levantar un servidor local que me sirva hacer pruebas utilizo el siguiente script que está en el PATH como start-server

#! /home/notxor/opt/bin/csi -s

(import spiffy
        simple-directory-handler)

(handle-directory simple-directory-handler)
(root-path ".")
(start-server)

Invocando el intérprete

Al instalar Chicken Scheme nos encontraremos dos binarios fundamentales1 y algunos otros auxiliares. El comando csi llama al intérprete de Chicken Scheme:

> csi
CHICKEN
(c) 2008-2021, The CHICKEN Team
(c) 2000-2007, Felix L. Winkelmann
Version 5.3.0 (rev e31bbee5)
linux-unix-gnu-x86-64 [ 64bit dload ptables ]

Type ,? for help.
#;1> 42
42
#;2> e

Error: unbound variable: e
#;2> 

Evaluando expresiones

Al ejecutarse en modo interactivo, nos muestra un breve mensaje con información sobre el copyright y la versión. Nos avisa de que podemos acceder a la ayuda si en el prompt tecleamos ,? y nos muestra el prompt: #;1>. Pulsamos la secuencia de teclas 4, 2, ENTER. Al pulsar esta última tecla, el sistema entiende que tiene que evaluar la línea y eso hace. Al evaluar un número o una variable cualquiera, nos devuelve su valor. Además, tras haber evaluado la primera línea, vemos que la numeración del prompt avanza.

Sin embargo, al escribir la secuencia e, ENTER, nos devuelve la frase Error: unbound variable: e y el contador del prompt no avanza. Lo que ocurre es que el sistema entiende que queremos evaluar una variable e que no está definida y nos advierte de ello.

Todo esto es muy básico y funciona de la manera esperada para quien haya utilizado otros sistemas REPL de otros lenguajes de programación.

Para salir del intérprete podemos evaluar la combinación ,q en el prompt o pulsar C-d como en la mayoría de terminales.

Primer archivo de código

Además de poder trabajar directamente en el prompt interactivo, podemos escribir el código en un archivo de texto plano con nuestro editor favorito. Tradicionalmente, los archivos de código de Scheme llevan la extensión .scm. También es tradicional, para crear el primer programa, utilizar el famoso hola mundo, pero como estoy un poco harto de él, vamos a hacer otra cosa: ¡vamos a jugar a la ruleta!2. Para ello creamos un nuevo fichero con el nombre ruleta.scm en el editor y escribimos el siguiente código:

(import (chicken random))

(define (ruleta)
  (pseudo-random-integer 37))

Hay algunos puntos que, supongo, necesitan un poco más de aclaración: import nos permite importar a nuestro programa paquetes, módulos o librerías que nos simplifican el trabajo. Aquellos módulos que vienen junto con Chicken3 se importan mediante la forma (chicken [módulo]). En el caso de nuestro ejemplo: (chicken random) importa un módulo que nos permite generar números pseudoaleatorios. Es decir, nos va a permitir utilizar la función pseudo-random-integer, que nos devolverá un número entero entre 0 y el rango máximo especificado (37 en nuestro caso).

La macro define permite definir tanto variables como funciones. No hay distinción entre ambas cosas, pues para Scheme no hay distinción entre código y datos4. Es decir, el sistema, al evaluar ruleta evaluará lo que contenga, que puede ser un valor o algo que se evalúe hasta encontrar un valor.

En el código se puede apreciar, que los nombres de entidades compuestos por varias palabras, dichas palabras se separan mediante un guión simple. Es una costumbre de todos los Lisp y derivados, no utilizar el CamelCase, puesto que para Lisp en origen todos los nombres se traducen a mayúsculas, y tampoco se utiliza el guión bajo —o la barra baja, si prefieres llamarlo así—.

Evaluar nuestro código en el intérprete

Una vez hemos escrito el código, es posible que queramos evaluar cómo funciona en el intérprete. Para ello debemos seguir estos sencillos pasos5:

  1. Lanzar el intérprete con el comando csi.
  2. Cargar nuestro código
  3. Evaluarlo
#;1> (load "ruleta.scm")
; loading ruleta.scm ...
; loading /[path al directorio]/lib/chicken/11/chicken.random.import.so ...
#;2> (ruleta)
11
#;3> 

La forma load nos permite cargar el código contenido en un archivo .scm. También se puede utilizar desde el modo interactivo la abreviatura o comando interno ,l. Es decir, podríamos haber hecho lo mismo de la siguiente manera:

#;1> ,l ruleta.scm
; loading ruleta.scm ...
; loading /[path al directorio]/lib/chicken/11/chicken.random.import.so ...
#;1> (ruleta)
32

La única diferencia entre los dos métodos es que utilizando el comando interno de csi, no se actualiza el contador del prompt, porque no ha habido evaluación de código Scheme. Pero en ambos casos, se evalúa el archivo ruleta.scm y se carga el módulo interno random como librería dinámica. Una vez cargado, podemos interactuar con nuestro código evaluando la forma (ruleta).

Jugar en la ruleta

Para poder jugar en la ruleta, aún nos falta un poco de código. Lo primero sería, poder decirle a nuestro programa a qué número queremos apostar. Para ello, deberíamos definir alguna forma que admita un parámetro. Por ejemplo:

(define (apostar-ruleta num-apostado)
  (comparar-apuesta (ruleta) num-apostado))

A la macro define se le pasa una lista con el nombre de la función seguida de los parámetros que necesita. En este primer ejemplo, sólo se necesita un parámetro: el num-apostado. A continuación se encuentra el cuerpo de la función, que lo único que hace es llamar a otra función —que aún no está programada— que necesitará dos parámetros.

(define (comparar-apuesta resultado num-apostado)
  (if (= resultado num-apostado)
      (display "¡Has ganado! Ha salido el ")
      (display "¡Has perdido! Ha salido el "))
  resultado)

En ésta última llamada, el primer parámetro es resultado, que, según hacemos la llamada en la función anterior, será el número obtenido en (ruleta). El segundo parámetro es, directamente, el que pasamos desde apostar-ruleta. El condicional if comprueba si ambos números en los parámetros son iguales. Para explicarlo un poco más, la forma if tiene tres argumentos:

  1. Una comparación que devuelve un booleano.
  2. El bloque de código que evaluará si ese booleano es #t.
  3. El bloque de código que evaluará si es #f.

Por último, la función comparar-apuesta también devuelve resultado, que como dije antes es el número obtenido en la ruleta. No hay una instrucción tipo return o similares, una función devuelve siempre el valor de la última evaluación que realiza y como sabemos, el nombre de una variable se evalúa con su valor.

Compilación a librería binaria

Si queremos que nuestro código se ejecute más rápido, podemos compilarlo en una librería dinámica. Ese código binario, se puede cargar también en el REPL, como ya hemos visto que hace Chicken cuando carga algún módulo interno. Por hacerlo brevemente:

> csc -s ruleta.scm
> csi
csi
CHICKEN
(c) 2008-2021, The CHICKEN Team
(c) 2000-2007, Felix L. Winkelmann
Version 5.3.0 (rev e31bbee5)
linux-unix-gnu-x86-64 [ 64bit dload ptables ]

Type ,? for help.
#;1> ,l ruleta.scm
; loading ruleta.scm ...
; loading /home/notxor/opt//lib/chicken/11/chicken.random.import.so ...
#;1> (apostar-ruleta 13)
¡Has perdido! Ha salido el 2
#;2> 

Explicándolo un poco más: csc es el compilador de Chicken Scheme. Con esta herramienta podemos compilar código proporcionando salida a .o, a ejecutable directamente o a librearías estáticas y dinámicas. En nuestro caso, como el código lo queremos cargar desde el modo interactivo, lo podríamos compilar como una librería dinámica de la siguiente manera:

> csc -s ruleta.scm

Como digo, csc es el compilador de Chicken Scheme. En el ejemplo, la opción -s (o la opción -shared, o -dynamic) genera una librería dinámica en nuestro sistema (ruleta.so en GNU/Linux, o .dll en Windows, o .dylib en Mac OS).

Esta nueva librería la podemos cargar también en el intérprete. Antes, vimos cómo cargar un módulo de los que vienen junto con Chicken al hacer una importación. Automáticamente se hacía un load y cargaba un archivo .so. Para cargar el nuestro, podemos hacerlo directamente:

#;1> ,l ruleta.so
; loading ruleta.so ...
#;1> (ruleta)
28
#;2> 

Lo podemos importar no sólo con la instrucción ,l, sino también con la forma equivalente (load "ruleta.so").

Compilar ejecutable

El código de nuestro primer ejemplo está pidiendo poder ejecutarse como un programa independiente desde un terminal. Para convertirlo en un ejecutable, necesitamos dos cosas:

  1. Recibir parámetros desde la línea de comandos.
  2. Establecer una función que será la entrada de la ejecución del binario. Tradicionalmente en C y otros lenguajes de programación, esta función se llama main.

Para cumplir con el primer requisito necesitamos importar un módulo interno de Chicken que se llama process-context. En este módulo se define commond-line-arguments, que devuelve una lista de cadenas que representan los parámetros que se escriben en la línea de comandos.

Empecemos por lo fácil. La función principal, la podemos escribir de la siguiente manera:

(define (principal args)
  (print (apostar-ruleta (string->number (car args)))))

A los que tienen un poco de reparo con los paréntesis, quizá les resultaría más fácil de entender la función si la escribimos así:

(define (principal args)
  (print
   (apostar-ruleta
    (string->number
     (car args)
    )
   )
  )
)

Evaluando el código de dentro hacia afuera nos encontramos:

  1. (car args) devuelve el primer parámetro recibido en la función.
  2. Puesto que el parámetro es una cadena y apostar-ruleta está pidiendo un número, debemos convertir la cadena en número con la función string->number.
  3. apostar-ruleta toma el número que le pasamos con el parámetro y lo compara con el número que ha salido en la ruleta.
  4. print muestra por pantalla el número obtenido en la ruleta al llamar a apostar-ruleta.

Lo que nos falta es decirle al compilador qué función es la que debe tomar como entrada del ejecutable. Eso lo hacemos con el siguiente código:

(cond-expand
  ((and chicken compiling)
   (principal (command-line-arguments)))
  (else #f))

cond-expand es un condicional, como indica su nombre. Lo que hace es que mientras se ejecuta el compilador de Chicken se añada una llamada a principal con la lista de argumentos y que no haga nada en caso contrario.

Al compilarlo desde la línea de comandos comprobamos si funciona.

> csc ruleta.scm
> ./ruleta 13
¡Has perdido! Ha salido el 2
>  

En este caso, al compilador csc lo llamo sólo pasándole el archivo que debe compilar sin ningún parámetro adicional. Eso genera el binario ruleta y se le llama desde línea de comandos como a cualquier otro programa.

El código completo del ejemplo es el siguiente:

(import (chicken random)
        (chicken process-context))

(define (ruleta)
  (pseudo-random-integer 37))

(define (comparar-apuesta resultado num-apostado)
  (if (= resultado num-apostado)
      (display "¡Has ganado! Ha salido el ")
      (display "¡Has perdido! Ha salido el "))
  resultado)

(define (apostar-ruleta num-apostado)
  (comparar-apuesta (ruleta) num-apostado))

(define (principal args)
  (print (apostar-ruleta (string->number (car args)))))

;; Ejecutar 'principal' cuando se compila como ejecutable
(cond-expand
  ((and chicken compiling)
   (principal (command-line-arguments)))
  (else #f))

Igualmente podemos seguir compilando este código como si fuera una librería. Lo único que sucederá es que al cargar la librería binaria en el intérprete, éste se quejará porque no le pasamos un parámetro por línea de comandos. Sin embargo el código sigue funcionando:

> csc -s ruleta.scm
> csi
CHICKEN
(c) 2008-2021, The CHICKEN Team
(c) 2000-2007, Felix L. Winkelmann
Version 5.3.0 (rev e31bbee5)
linux-unix-gnu-x86-64 [ 64bit dload ptables ]

Type ,? for help.
#;1> ,l ruleta.so
; loading ruleta.so ...

Error: (car) bad argument type: ()

        Call history:

        ruleta.scm:22: chicken.process-context#command-line-arguments	 
        ruleta.scm:22: principal	 
        ruleta.scm:17: scheme#string->number	 	<--
#;1> (apostar-ruleta 13)
¡Has perdido! Ha salido el 30
#;2> 

Depuración

Chicken Scheme proporciona también una aplicación que nos ayuda a depurar el código. Dicha aplicación depende de Tcl/Tk y debes tener instalado en tu sistema dicho entorno. Además, cuando compilamos, debemos establecer también el nivel de depurado:

> csc -d3 ruleta.scm
> feathers ruleta 13
Captura_debugger.png

Habitualmente se abre sólo la ventana de código, pero al pulsar <F3>, como se aprecia en la imagen, también podemos inspeccionar el valor de las variables.

Conclusiones

Chicken Scheme es un sistema muy flexible y rápido. Proporciona herramientas sencillas de manejar y de poner a prueba y cuenta con una documentación bastante amplia. Muchas veces, mi forma de trabajar es ir haciendo pruebas al estilo REPL.

Mi entorno de trabajo es Emacs con el paquete Geiser y con eso me es suficiente. Pero hay quien necesita herramientas externas y un lsp6 que llevarse al editor. Chicken Scheme cuenta con la posibilidad de instalar su lsp. Sólo hay que ejecutar el comando:

> chicken-install lsp-server

Una vez instalado el servidor debes configurarlo para que funcione en tu editor favorito.

Notas al pie de página:

1

El compilador csc y el intérprete csi.

2

Abstenerse los ludópatas, o al menos, no realizar apuestas con el código.

3

Los llamados módulos internos.

4

Ocurren también en más sistema de la familia Lisp. No hay una marcada frontera entre datos y código.

5

En Emacs el paquete geiser nos facilita todos estos asuntos permitiéndonos comunicarnos con el REPL mientras escribimos código en él.

6

Language Server Protocol

Categoría: scheme chicken

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.