4 de febrero de 2013

String Calculator en Kawa

Hace un par de días, un amigo nuestro compartió en su blog de Java México, su solución a la Kata String Calculator en Groovy. Esto me motivó a compartir también mi solución en Kawa y compararlas para ver si las mismas ideas se podrían implementar/traducir en/a Groovy.

Cabe mencionar que yo también me tardé mas de media hora en resolver el ejercicio (dos pomodoros de 20 min c/u), lo cual confirma la teoría que teníamos con un amigo sobre el tiempo que sería necesario para este ejercicio en un Coding-Dojo.

He aquí el código fuente en Scheme Kawa:
; -*- coding: utf-8; mode: Scheme -*-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Coding Kata String Calculator 

(require 'srfi-1 ) ;; List Library
(require 'srfi-13) ;; String Library
(require 'srfi-14) ;; Character-set Library

;; (define-simple-class StringCalculator ()
;;   ;; methods
;;   ((Add numbers ::String) ::int allocation: 'static

(define (Add numbers ::String) ::int
  (let* ((lstnum (filter (lambda (n)
                           (if (<= n 1000) #t #f))
                         (map string->number
                              (string-tokenize 
                               numbers
                               (char-set-adjoin char-set:digit #\-)))))
         (negativos (filter negative? lstnum)))
    (if (null? negativos)
        (apply + lstnum)
        (throw (java.lang.RuntimeException 
                (format #f 
                        "No se permiten negativos: ~s~%" 
                        negativos))) )))

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Pruebas Unitarias
(require 'srfi-64) ;; Testing Library

;;(define-alias Add StringCalculator:Add)

(test-begin "string-calculator")
;;(test-assert "Prueba no implementada" #f)
;;Req 1.1 y 1.2
(test-equal 0 (Add "Hola"))
(test-equal 0 (Add ""))
(test-equal "Suma '1'" 1 (Add "1"))
(test-equal "Suma '1,2'" 3 (Add "1,2"))
;;Req 2 y 3
(test-equal "Suma '1,2,3'" 6 (Add "1,2,3"))
(test-equal "Suma '1\n2,3'" 6 (Add "1\n2,3"))
;;Req 4
(test-equal 3 (Add "//;\n1;2"))
;;Req 6, 8 y 9
(test-equal 2 (Add "2,1001"))
(test-equal 6 (Add "//[*][%]\n1*2%3"))
;; pruebas del script de groovy
(test-equal 0 (Add "")) ;;assert 0 == "".sum()
(test-equal 0 (Add " ")) ;;assert 0 == " ".sum()
(test-equal 1 (Add "1")) ;;assert 1 == "1".sum()
(test-equal 1 (Add " 1")) ;;assert 1 == " 1".sum()
(test-equal 1 (Add " 1 ")) ;;assert 1 == " 1 ".sum()
(test-equal 1 (Add "  1  ")) ;;assert 1 == "  1  ".sum()
(test-equal 0 (Add "a")) ;;assert 0 == "a".sum()
(test-equal 3 (Add "1,2")) ;;assert 3 == "1,2".sum()
(test-equal 1 (Add "1, a")) ;;assert 1 == "1, a".sum()
(test-equal 2 (Add "a, 2")) ;;assert 2 == "a, 2".sum()
(test-equal 6 (Add "1,2,3")) ;;assert 6 == "1, 2, 3".sum()
(test-equal 6 (Add "//;\n1, 2, 3")) ;;assert 6 == "//;\n1, 2, 3".sum()
(test-equal 15 (Add "//;'\n1, 2, 3' 4' 5")) ;;assert 15 == "//;'\n1, 2, 3' 4' 5".sum()
(test-equal 15 (Add "//;'\n1, 2, 3' 4' 5, \n'")) ;;assert 15 == "//;'\n1, 2, 3' 4' 5, \n'".sum()
(test-equal 15 (Add "//;'\n1, 2, 3' 4' 5, \n', a, b; c' d")) ;;assert 15 == "//;'\n1, 2, 3' 4' 5, \n', a, b; c' d".sum()
(test-equal 0 (Add ",;\n")) ;;assert 0 == ",;\n".sum()
(test-error  (Add "-1, 2, 3")) ;;try{ "-1, 2, 3".sum()
(test-error  (Add "-1, 2, 3, -4, -5")) ;;try{ "-1, 2, 3, -4, -5".sum() }catch(e){ println e}
(test-error  (Add "//;'\n1, 2, -3' -4' 5")) ;;try{ "//;'\n1, 2, -3' -4' 5".sum() }catch(e){ println e}

(test-end "string-calculator")


Notas:
  • Escribir una clase con un solo método (que además es estático) es una mala práctica. Aunque en este caso el requerimiento así lo estipule, he preferido dejar el método Add como una función ordinaria de Scheme y dejar el código de definición de clase y método comentados solo como ejemplo de como se hace en Kawa.
  • Agregué al final de las pruebas unitarias, la traducción a Scheme de las pruebas del script de Groovy para comprobar que el código cumple con los mismos requerimientos. E.d. Pasa las mismas pruebas

El suponiendo que código anterior se guarde en un archivo llamado "string-calc.scm", éste puede ser ejecutado desde la línea de comandos de la siguiente forma:
$ kawa -f string-calc.scm
Produciendo el siguiente resultado:
%%%% Starting test string-calculator  (Writing full log to "string-calculator.log")
# of expected passes      28
Hasta aquí la coding kata de hoy.

Saludos cordiales.

2 comentarios:

  1. * Lo primero que me "bota" al ver el código son las líneas:

    (require 'srfi-1 ) ;; List Library
    (require 'srfi-13) ;; String Library
    (require 'srfi-14) ;; Character-set Library

    y

    (require 'srfi-64) ;; Testing Library

    interesante nomenclatura!

    * Bueno, ahora algo menos mundano: efectivamente, la forma en que están redactadas las especificaciones de la mayoría de las Katas más populares, asumen que los lectores estarán familiarizados con lenguajes mainstream como Java o C# en los cuales el uso de las clases no es opcional. Creo que esto debe considerarse como una atención a la mayoría de los lectores potenciales más que como parte de los requerimientos de la kata en sí.

    * Python y otros lenguajes cuentan con la facilidad de crear clases cuyas instancias pueden ser usadas como funciones. El hecho de que esto "pueda hacerse" nunca debe confundirse con "debe hacerse", como bien lo mencionas. Una de las malas costumbres que traje de Java a otros lenguajes es precisamente el querer hacer que absolutamente todo fuera una clase, cosa que afortunadamente y con paciencia y ayuda de mis amigos, comienzo a superar :)

    *El primer término del let inicial, definitivamente requiere un poco de descomposición. Para mi, que aún no puedo leer la matrix, es simplemente demasiado complejo ese one-liner, incluso si lo tradujera a python.

    * Una parte que no entiendo del sintaxis es la parte "#\-" al final de la llamada a char-set-adjoin (me imagino que dice "parte la cadena incluyendo todos los dígitos y el signo de negación en cada sub-cadena, pero tb se incluirían el "#" y el "\"? o es una convención para designar una literal de caracter?).

    * El requerimiento 4 no me parece que esté explícitamente cubierto en el código. Más bien me parece que está implícito precisamente en la funcionalidad de string-tokenize. El planteamiento de la kata no especifica ninguna provisión en cuanto al manejo de errores, por lo que el código maneja cualquier carácter no numérico (y creo que excluyendo el signo "-", según mi punto anterior) como delimitador, aún cuando este no haya sido "declarado" explícitamente en la primera linea de la cadena. Ditto para el req. 8.

    No parece haber ninguna provisión para los reqs. 7 y 9 ni en el código ni en los tests.

    ResponderEliminar
  2. Saludos mi estimado Alfredo

    Muchas gracias por tus valiosas observaciones.
    Por este medio intentaré aclarar algunas de las dudas que planteas.

    * La nomenclatura de las bibliotecas estándar de Scheme versión R5 es un tanto críptica y corresponde las siglas del Scheme Request For Implementation que le corresponden a cada una. Se espera que en el estándar R7 se provean nombres mas descriptivos.

    * En cuanto al enunciado mismo de la Kata, creo que éste requiere una transcripción mas clara y menos ambigua. P.ej. no dice que hacer con las entradas inválidas (solo dice "not need to prove it"), y como tu bien mencionas, no especifica tampoco nada respecto al manejo de errores, solo que se debe levantar una excepción. El punto de los delimitadores IMHO no es lo suficientemente claro. Y bueno, ya desde allí tendríamos que empezar con un enunciado mas claro del ejercicio y especificar mejor algunas cosas.

    * Definitivamente la expresión del primer binding de la forma LET* requiere refactorización.

    * La sintaxis de Scheme para el tipo de dato caracter es el prefijo #\ de donde la expresión #\- solamente indica el caracter "-" (http://www.ccs.neu.edu/home/dorai/t-y-scheme/t-y-scheme-Z-H-4.html#node_sec_2.1.3)

    * Creo que esta forma de solución ha sido un poco "mañosa" debido a que se aprovecha de las inconsistencias en el planteamiento del problema y se limita a filtrar todos los tokens numéricos (positivos y negativos) sin importarle los delimitadores, y al final el resultado (de la suma) es el mismo. Por lo que es sub-óptima si consideramos el _espíritu_original_ del ejercicio (otra vez: hay que replantear bien el problema de forma clara y no-ambigua).

    ResponderEliminar