Notxor tiene un blog

Defenestrando la vida


Código para pasar prueba por pantalla

Bueno, para seguir con la serie sobre la corrección de pruebas psicológicas por pantalla, ya tengo algunas cosillas hechas y lo muestro por aquí. En las entradas anteriores solté un ladrillo sobre cómo iba a hacer las cosas y bla, bla, bla. Todo mentira: realmente sabes cómo van a ir las cosas cuando las haces, las pruebas, no funcionan, corriges y las haces funcionar... En fin, de lo pensado a lo hecho hay algunos cambios, así pues: voy a ver si me explico con claridad. El código (casi) completo de lo que hay hecho hasta ahora en el proyecto está guardado en un fichero minimult.el y lo pondré al final de esta entrada.

Depurar con edebug

Una de las cosas que he tenido que aprender sobre la marcha es la depuración de los errores que he ido cometiendo por el camino. Algo tan importante a la hora de programar como el escribir código.

Emacs lleva incorporado su depurador. En realidad y porque la gente de Emacs es así, trae varios. Como estoy acostumbrado a depuradores más visuales, me decidí a utilizar edebug. En el manual están todos los comandos que admite, aunque básicamente he utilizado tres:

  • edebug-defun: En cualquier posición de la definición de la función llamas a M-x edebug-defun y cuando se ejecuta la misma comienza la depuración.
  • n: Avanza la ejecución de instrucción en instrucción.
  • : pulsando la tecla del espacio hace más paradas entre las expresiones de una misma instrucción. La ventaja es que va mostrando en el área de eco el resultado de evaluar la expresión o variable y puedes ir viendo cómo cambian sus valores.

También puedes poner puntos de parada (breakpoint) con el comando b y hacer que termine todo el proceso con el comando g (go). Básicamente para poner un punto de parada más adelante y hacer que todo se ejecute hasta que llegue allí.

Como impresión general edebug me ha dejado una buena sensación. Me llamó la atención lo de preparar la función para ser depurada. Acostumbrado a poner puntos de parada en el sitio que quieres depurar y lanzar el programa, me lo tomé como «poner punto de parada al inicio de la función». Ahora, después de llevar utilizándolo una semana, me parece hasta natural.

Pero no seré yo el que le puede poner «peros» a nada. Yo soy psicólogo y lo de la programación es una afición. Sin embargo, creo que es una buena herramienta, que merece no una, sino varias entradas en un blog como éste. De momento, a la documentación me remito.

Analizando un poco lo que he hecho

Es complicado hablar detalladamente de todo el proceso hasta llegar al código y seguro que cualquier informático le encontrará diez fallos a cada línea que he escrito.

Lo primero que he tenido que hacer es acostumbrarme a la sintaxis de Lisp, que en diez minutos la aprendes pero al final la falta de costumbre te hace cometer errores. Parece que siempre te faltan paréntesis (aún siendo cuidadoso como es mi caso).

De primeras las respuestas podían ser «V», «F» y « ». Sin embargo, los espacios son un problema. Cuando se guardan las respuestas en una cadena y se meten en una «propiedad» dentro del árbol del fichero org resulta que se guarda sin nada que indique dónde empieza y dónde acaba.

De lo primero me di cuenta después. Primero me di cuenta de que si guardaba la prueba a medias se ignoraban los espacios en blanco posteriores. Lo intenté solventar comprobando cuándo se acaban las respuestas, pero el problema es aún más grave cuando las primeras preguntas se dejan en blanco: esos caracteres son ignorados y por tanto el resto de las respuestas son asignadas a preguntas que no corresponden. Por esto, decidí utilizar el carácter «-» en lugar del espacio en blanco.

La forma de presentar las preguntas en pantalla estuve a punto de decidirme por hacerlo de forma recursiva con la lista de preguntas (sólo por probar), pero al final me decidí por un bucle while cuyo meollo es el siguiente:

(setq lista-widgets ;; Crear un lista de widgets para las respuestas
 (cons         ;; Estará en orden inverso
  (widget-create 'editable-field
	 :tag (concat "P" (number-to-string (+ contador 1)))
	 :size 1
	 :value (if (and minimult-respuestas (< contador (length minimult-respuestas)))
		(char-to-string (elt minimult-respuestas contador))
	    "-"))
   lista-widgets))
(widget-insert " ")
(widget-insert (elt minimult-preguntas contador)) ;; extraer la pregunta de CONTADOR

Esa parte del código repetida tantas veces como preguntas hay que presentar hace el trabajo de presentar por pantalla la pregunta asociada a un widget (mostrando una posible respuesta anterior).

El proceso de corrección sería el siguiente:

  1. Cargar el código. Ahora mismo lo tengo en un directorio externo a Emacs. Por eso lo cargo con el comando M-: (load "path/completo/al/fichero.el") que es una de las posibles formas de añadir código al entorno.
  2. Lanzar el proceso. Como la función está declarada como interactiva se la llama con el comando M-x minimult-pasar-prueba. Como se puede ver en la siguiente imagen

    minimult-lanzado.png

    Al llamar a la función se inserta en :PROPERTIES: una entrada para :Test: con el valor MiniMult.

  3. Contestar prueba. La prueba se puede contestar pulsando apenas tres teclas para pasar a la siguiente, v y f. Se puede hacer en minúsculas porque cuando se guardan se pasan a mayúsculas. Como se puede ver en la siguiente imagen.

    minimult-contestando.png

  4. Guardar respuestas. Una vez se termina de contestar sólo hay que pulsar el botón que se puede ver en la imagen anterior: [Guardar respuestas]. Esto hará que aparezca otra entrada en :PROPERTIES: para :Respuestas: con la cadena formada por las respuestas.

    minimult-respuestas.png

    Cuando se guardan las respuestas, automáticamente se cierra el buffer y vuelve a la historia clínica.

Algunos puntos mejorables

Uno de los puntos mejorables es la presentación de la prueba. Quizá no consistiría más que en introducir algún salto de línea y algunos espacios para que las preguntas largas no se escapen por la derecha de la ventana.

Otro que cuando acaba de cargar la prueba el punto de edición se queda al final del buffer. Sólo con pulsar regresa al espacio reservado para el primer ítem.

Una de las limitaciones del sistema que me he encontrado es que aunque se dibujan los widgets con tamaño 1, si se escriben más caracteres el campo se expande. En la mayoría de los casos será el comportamiento deseado, pero en este ejercicio sólo queremos un carácter. Por eso, cuando se lee el contenido del mismo lo que se hace es cortar el primer carácter.

Otra mejora que se podría hacer es que también entendiera los caracteres S y N como respuesta (y no se descarta hacerlo más adelante).

Código

Antes dije que estaría casi todo el código. Lo que falta es que la variable que guarda las preguntas, como ya la puse en otra entrada, pondré la primera y última un unos puntos suspensivos.

(require 'widget)

(eval-when-compile
  (require 'wid-edit))

(defvar lista-widgets)

(defvar buffer-original)

(defvar minimult-respuestas)

(defvar minimult-preguntas
  '("Tengo buen apetito."
  ....
    "He abusado del alcohol.")
  "Listado de preguntas del MiniMult.")

(defun minimult-pasar-prueba ()
  "Muestra la prueba MiniMult por pantalla y guarda las respuestas."
  (interactive)
  (setq minimult-respuestas                   ;; variable que guardará las respuestas
    (org-entry-get (point) "Respuestas")) ;; si no hay respuestas anteriores será nil.
  (org-entry-put (point) "Test" "MiniMult")   ;; en la historia hace figurar un test MiniMult
  (setq lista-widgets '())
  ;; Guardar el buffer viejo
  (setq buffer-original (current-buffer))
  (let ((contador 0))
    ;; Crear buffer temporal
    (switch-to-buffer "*MiniMult*")
    (kill-all-local-variables)
    (overwrite-mode)
    ;; (setq inhibit-read-only t)
    (erase-buffer)
    (remove-overlays)
    ;; Iniciar el formulario con unas breves instrucciones
    (widget-insert "Responda sinceramente a estas preguntas. No hay tiempo, leealas despacio\n")
    (widget-insert "y no se preocupe por el tiempo. Habrá ocasiones en que no pueda decidirse\n")
    (widget-insert "por una u otra opción. En estos casos la primera que llegue a nuestra mente\n")
    (widget-insert "es la que deberíamos consignar. Ante cualquier duda pregunte al evaluador.\n\n")

    ;; Rellenar el buffer con las preguntas
    (while (< contador (length minimult-preguntas))
      (if (< contador 9)        ;; Hay que recordar que el contador se inicia en 0
      (widget-insert " "))  ;; por eso alineamos con un espacio las primeras 9 preguntas
      (widget-insert (number-to-string (+ contador 1))) ;; y sumamos 1 a CONTADOR para poner el número
      (widget-insert ".-")
      (setq lista-widgets ;; Crear un lista de widgets para las respuestas
	(cons         ;; Estará en orden inverso
	 (widget-create 'editable-field
		:tag (concat "P" (number-to-string (+ contador 1)))
		:size 1
		:value (if (and minimult-respuestas (< contador (length minimult-respuestas)))
		       (char-to-string (elt minimult-respuestas contador))
		     "-"))
	 lista-widgets))
      (widget-insert " ")
      (widget-insert (elt minimult-preguntas contador)) ;; extraer la pregunta de CONTADOR
      (widget-insert "\n")
      (setq contador (1+ contador)))
    (widget-create 'push-button
	   :notify (lambda (&rest ignore)
		 (let ((contador 0)
		       (total (length lista-widgets))
		   (respuestas "")
		   (item ""))
		   (while (< contador total)
		     ;; Obtener el primer carácter del campo
		     (setq item
		       ;; Comprobar que hay un valor
		       (if (widget-value (car lista-widgets))
			   ;; Eliminar el widget de la lista obteniendo su valoro
			   (substring (widget-value (pop lista-widgets)) 0 1)
			 "-"))
		     ;; Encadenamos cada respuesta en una cadena
		     (setq respuestas (concat (upcase item) respuestas))
		     ;; Actualizamos contador
		     (setq contador (1+ contador)))
		   ;; Cambiamos al buffer anterior
		   (set-buffer buffer-original)
		   ;; Guardamos respuestas
		   (org-entry-put (point) "Respuestas" respuestas)
		   ;; Destruimos el buffer temporal
		   (kill-buffer "*MiniMult*")))
	   "Guardar respuestas")
    (use-local-map widget-keymap)
    (widget-setup)
  ))

Comentarios