Notxor tiene un blog

Defenestrando la vida


Aprendiendo scheme: un ejercicio simple

Un ejercicio simple

El otro día un amigo (dddddd, en las redes) me comentó un ejercicio sencillo que se podría hacer en scheme para ir aprendiendo. El caso es que él está con el diseño de un procesador de 12 bits junto con otra persona. Ya tiene un emulador funcionando y me comentó que estaba con un código que convertía la cadena que llegaba en valores de 12 bits. Él lo hizo en ensamblador de su máquina, sin embargo me dijo que sería un buen ejercicio para hacerlo en scheme. Las premisas son sencillas:

  1. Llega una cadena alfanumérica que hay que convertir en su correspondiente valor numérico o devolver una lista de valores si la cadena es más larga.
  2. Entre los valores numéricos pueden aparecer espacios en blanco que hay que ignorar.
  3. Es una función máquina así que no debería depender de ninguna librería.

El punto 3, como veremos a continuación me lo salté. ¿Por qué? Básicamente me interesa a mí, aprender scheme, seguir la lógica de programación de ensamblador no me ayuda. Así que puse mis condiciones:

  1. Iba a utilizar chicken scheme con el mínimo de «librerías posible», pero de más alto nivel que el ensamblador.
  2. Iba a utiliza programación literaria utilizando el modo org de emacs: básicamente para saber cómo funciona y cómo de útil es este tipo de programación.
  3. Iba a utilizar la programación funcional todo lo que pudiera con el objeto de acostumbrar mi cerebro mononeuronal a ese tipo de programación (y porque también es uno de los objetivos de aprender scheme).

Las pruebas

Comencé escribiendo unas pruebas sencillas. Lo primero, hacer una función que devolviera el valor numérico de cada char. Es decir, si llega un carácter '3' debería devolver el valor 3. Tampoco voy a detallar todos los ciclos de desarrollo y refactorizaciones del código para llegar al código final. Pero pongo aquí el fichero de pruebas que he utilizado para hacer un desarrollo TDD del código.

(require-extension test)
(require "conversor.scm")

(test-group "Convertir un char a su valor numérico"
            (test 0 (convertir-char #\0))
            (test 1 (convertir-char #\1))
            (test 2 (convertir-char #\2))
            (test 3 (convertir-char #\3))
            (test 4 (convertir-char #\4))
            (test 5 (convertir-char #\5))
            (test 6 (convertir-char #\6))
            (test 7 (convertir-char #\7))
            (test 8 (convertir-char #\8))
            (test 9 (convertir-char #\9))
            (test 10 (convertir-char #\a))
            (test 10 (convertir-char #\A))
            (test 11 (convertir-char #\b))
            (test 11 (convertir-char #\B))
            (test 12 (convertir-char #\c))
            (test 12 (convertir-char #\C))
            (test 13 (convertir-char #\d))
            (test 13 (convertir-char #\D))
            (test 14 (convertir-char #\e))
            (test 14 (convertir-char #\E))
            (test 15 (convertir-char #\f))
            (test 15 (convertir-char #\F)))

(test-group "Caracteres no numéricos"
            (test '() (convertir-char #\n))
            (test '() (convertir-char #\m)))

(test-group "Convertir una cadena de tres cifras a su valor numérico"
            (test 0 (calcular-numero "000"))
            (test 256 (calcular-numero "100"))
            (test 257 (calcular-numero "101")))

(test-group "Dividir una cadena en grupos de tres"
            (test '("000") (dividir-cadena "000"))
            (test '("111" "222" "333") (dividir-cadena "111222333"))
            (test '("100" "010" "001") (dividir-cadena "100010001"))
            (test '("ddd" "ddd") (dividir-cadena "dddddd"))
            ;; Una cadena de menos de tres caracteres devolverá una lista vacía
            (test '() (dividir-cadena "12")))

(test-group "Convertir cadena en una lista de números"
            (test '(563 288 255) (conversor "23 3120 0 f f"))
            (test '(0 16 256) (conversor "00 0 01 010 0")))

;; Asegurarnos de que las pruebas automáticas pueden distinguir un fallo de un cierre normal
(test-exit)

El código

El código final lo pongo a continuación.

(require-extension srfi-13)  ; <- extensión necesaria para trabajar con cadenas

;; Devuelve el valor numérico de un char
(define (convertir-char c)
  (case c
    ((#\0) 0)
    ((#\1) 1)
    ((#\2) 2)
    ((#\3) 3)
    ((#\4) 4)
    ((#\5) 5)
    ((#\6) 6)
    ((#\7) 7)
    ((#\8) 8)
    ((#\9) 9)
    ((#\a #\A) 10)
    ((#\b #\B) 11)
    ((#\c #\C) 12)
    ((#\d #\D) 13)
    ((#\e #\E) 14)
    ((#\f #\F) 15)
    (else '())))

;; Devuelve el valor numérico de una cadena de tres cifras
(define (calcular-numero cadena)
  (apply + (map * '(256 16 1)
                (map convertir-char
                     (string->list cadena)))))

;; Devuelve una lista de cadenas de tres caracteres
(define (dividir-cadena cadena)
  (if (< (string-length cadena) 3)
      '()
      (cons (string-take cadena 3) (dividir-cadena (string-drop cadena 3)))))

;; Devuelve una lista de números dada una cadena alfanumérica
(define (conversor cadena)
  (map calcular-numero (dividir-cadena (string-delete #\space cadena))))

Destacar que conseguí evitar toda tentación de escribir un sólo bucle, especialmente en la función dividir-cadena, donde el bucle necesario se implementó con una función recursiva.

Programación literaria

No me voy a poner a explicar lo que es la programación literaria y todas sus implicaciones. Básicamente consiste en escribir un documento donde se cuenta qué hace el código, con el código intercalado. Después, una aplicación separa el texto del código. En mi caso ha sido emacs y su org-mode el que me ha permitido hacer todo el proceso.

El punto positivo lo ha demostrado cuando he dejado el ejercicio durante tiempo. Cuando retomas un código que hace días que no has trabajado tienes que repasarlo entero e intentar acordarte de qué estabas pensando en el momento que lo escribiste. Con la programación literaria es más fácil, porque he escrito lo que estaba pensando antes de escribir el código, cómo se podría refactorizar, cómo se debería continuar, etc. Cuando retomaba el trabajo, leía lo que había escrito y rápidamente sabía qué hacía el código, qué había querido hacer yo y qué había que hacer para continuar.

La sensación es positiva en general, sin embargo, hay ocasiones en las que una refactorización deja obsoleto texto que has escrito antes y que en una posterior lectura puede provocar confusiones. Hay que ser muy cuidadoso y revisar todo el texto en las refactorizaciones si no se quiere arrastrar inconsistencias en las explicaciones. Eso, requiere un poco de trabajo extra. En contrapartida, tienes la mejor documentación posible sobre el código, la haces a la vez que el código en el mismo proceso. Eso evitará la pereza de escribir documentación después y siempre es más llevadero revisar algo escrito que redactar de cero todo.

Conclusión

Estoy contento con el resultado del experimento. La forma de trabajar ha sido fluida: los ratos en que me he podido poner con ello enseguida cogía el hilo. A ello contribuía tanto la explicación como la sencillez de cada función; código escueto con mucha literatura que lo explica.

Los ciclos de escribir tests, escribir código que supere los tests y refactorizar, se han hecho muy sencillos y rápidos.

Así que, bueno, ahí van mis adelantos con scheme y mis cambios en mis hábitos de escribir código.


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 Mastodon y en Diaspora con el nick de Notxor.

También se ha abierto un grupo en Telegram, para usuarios de esa red.

Disculpen las molestias.