Introducción a Chicken Scheme
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:
- Lanzar el intérprete con el comando
csi
. - Cargar nuestro código
- 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:
- Una comparación que devuelve un booleano.
- El bloque de código que evaluará si ese booleano es
#t
. - 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:
- Recibir parámetros desde la línea de comandos.
- 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:
(car args)
devuelve el primer parámetro recibido en la función.- 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ónstring->number
. 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.print
muestra por pantalla el número obtenido en la ruleta al llamar aapostar-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
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
lsp
6 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:
El compilador csc
y el intérprete csi
.
Abstenerse los ludópatas, o al menos, no realizar apuestas con el código.
Los llamados módulos internos.
Ocurren también en más sistema de la familia Lisp. No hay una marcada frontera entre datos y código.
En Emacs el paquete geiser
nos facilita todos estos asuntos
permitiéndonos comunicarnos con el REPL mientras escribimos código
en él.
Language Server Protocol
Comentarios