Nombres

Les nombres sont un type de données relativement basique. Guile met en oeuvre une Tour de types numériques, offrant au hacker pléthore de types de nombres avec leurs lots de fonctionnalités.

Ici, je me focaliserai sur les entriers et l’addition, mais libre à toi d’expérimenter avec ce dont tu as besoin. L’objectif de ce chapitre sera de créer une procédure integer-add, partiellement implémentée, et voir comment tout ça fonctionne.

Écrire le test d’abord

Donc, dans ton espace de travail, crées un répertoire ~/Workspace/guile-handbook/numbers où tu éditeras le fichier numbers-test.scm. Ci-dessous, le premier test a été ajouté. Il s’agit du cas qui me semble être le plus simple.

(use-modules (srfi srfi-64)
             (numbers))

(test-begin "harness-numbers")

(test-equal "add-zero-zero"
  0
  (integer-add 0 0))

(test-end "harness-numbers")

Tu peux remarquer que je fais appel au module numbers, il contiendra la fonction integer-add.

Essayer et lancer le test

$ guile -L . numbers-test.scm

En inspectant le message d’erreur retourné par le compilateur :

no code for module (numbers)

Écrire le minimum de code pour faire compiler le test et vérifier la raison de son échec

Crées maintenant le module numbers dans le fichier ~/Workspace/guile-handbook/numbers/numbers.scm pour satisfaire le compilateur et rien d’autre !

(define-module (numbers))

Une nouvelle exécution des tests m’indique que :

;;; numbers-test.scm:8:2: warning: possibly unbound variable `integer-add'

Réitères et chaque fois, tu te laisses guider par le compilateur jusqu’à voir ton test échouer pour la bonne raison. Le fichier ~/Workspace/guile-handbook/numbers/numbers.scm devient :

(define-module (numbers))

(define-public (integer-add int1 int2)
  -1)

Maintenant, on voit bien le test échouer, car, la valeur attendue est 0 mais la valeur obtenue est −1 (oui, c’est fait exprès !).

%%%% Starting test harness-numbers
Group begin: harness-numbers
Test begin:
  test-name: "add-zero-zero"
  source-file: "numbers-test.scm"
  source-line: 6
  source-form: (test-equal "add-zero-zero" 0 (integer-add 0 0))
Test end:
  result-kind: fail
  actual-value: -1
  expected-value: 0
Group end: harness-numbers
# of unexpected failures  1

Écrire juste assez de code pour faire passer le test

Tu vas maintenant modifier le minimun de code pour faire passer le test. Attention les yeux :

(define-module (numbers))

(define-public (integer-add int1 int2)
  0)

L'exécution du test prouve que cette modification est suffisante. C'est l'heure du…

Réusiner

Il y a peu de code ici, mais il y a quand même quelque chose de remarquable : la valeur zéro est codée en dure et laissée telle quelle. Pourtant, dans notre application, elle a une signification particulière, on dit une signification métier. En effet, pour l’addition, zéro est l’élément neutre. Je vais donc faire ressortir cette information explicitement.

(define-module (numbers))

(define-public (integer-add int1 int2)
  (let ((NEUTRAL_ELEMENT 0))
    NEUTRAL_ELEMENT))

Cette variable est localement définie dans la procédure integer-add, sa seule utilisation.

Écrire le test d’abord

Passons au deuxième test dans numbers-test.scm :

(use-modules (srfi srfi-64)
             (numbers))

(test-begin "harness-numbers")

(test-equal "add-zero-zero"
  0
  (integer-add 0 0))

(test-equal "add-one-zero"
  1
  (integer-add 1 0))

(test-end "harness-numbers")

Essayer et lancer le test

$ guile -L . numbers-test.scm

Écrire le minimum de code pour faire compiler le test et vérifier la raison de son échec

Pas d’erreur à la compilation. Le test échoue correctement !

[…]
Test begin:
  test-name: "test-interger-add-one-zero"
  source-file: "numbers-test.scm"
  source-line: 10
  source-form: (test-equal "add-one-zero" 1 (integer-add 1 0))
Test end:
  result-kind: fail
  actual-value: 0
  expected-value: 1

Écrire juste assez de code pour faire passer le test

La différence entre le premier et le deuxième test est la valeur du premier paramètre donné à integer-add. Voilà ce que je te propose : la valeur retournée dépend de la valeur de ce premier paramètre.

(define-module (numbers))

(define-public (integer-add int1 int2)
  (let ((NEUTRAL_ELEMENT 0))
    (if (equal? NEUTRAL_ELEMENT int1)
          NEUTRAL_ELEMENT
          1)))

Exécute les tests à nouveau.

$ guile -L . numbers-test.scm

Et voilà que tout passe !

%%%% Starting test harness-numbers  (Writing full log to "harness-numbers.log")
# of expected passes      2

Réusiner

Dans ce second test, j'ai utilisé la valeur 1 arbitrairement. J'aurais pu en choisir une autre, du moment que ce n'est pas l'élément neutre de l'addition. Je vais donc faire ressoirtir cette intention dans le nom d'une variable qui portera cette valeur.

Modifies le fichier numbers-test.scm :

(use-modules (srfi srfi-64)
             (numbers))

(define DUMMY_NON_NEUTRAL_VALUE 1)

(test-begin "harness-numbers")

(test-equal "add-zero-zero"
  0
  (integer-add 0 0))

(test-equal "add-one-zero"
  DUMMY_NON_NEUTRAL_VALUE
  (integer-add DUMMY_NON_NEUTRAL_VALUE 0))

(test-end "harness-numbers")

Après chaque petite passe de ton refactoring, il est recommander de lancer les tests pour s'assurer que tout est en ordre.

$ guile -L . numbers-test.scm 
;;; note: source file /home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm
;;;       newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling /home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm.go
%%%% Starting test harness-numbers  (Writing full log to "harness-numbers.log")
# of expected passes      2

On est bon. Maintenant, je me rend compte que la variable NEUTRAL_ELEMENT serait utile dans les tests également.

Modifies le fichier numbers.scm :

(define-module (numbers))

(define-public NEUTRAL_ELEMENT 0)

(define-public (integer-add int1 int2)
  (if (equal? NEUTRAL_ELEMENT int1)
      NEUTRAL_ELEMENT
      1))

Modifies le fichier numbers-test.scm :

(use-modules (srfi srfi-64)
             (numbers))

(define DUMMY_NON_NEUTRAL_VALUE 1)

(test-begin "harness-numbers")

(test-equal "add-zero-zero"
  NEUTRAL_ELEMENT
  (integer-add NEUTRAL_ELEMENT NEUTRAL_ELEMENT))

(test-equal "add-one-zero"
  DUMMY_NON_NEUTRAL_VALUE
  (integer-add DUMMY_NON_NEUTRAL_VALUE NEUTRAL_ELEMENT))

(test-end "harness-numbers")

Assures-toi que tu n'as pas cassé de tests :

$ guile -L . numbers-test.scm 
;;; note: source file /home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm
;;;       newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling /home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm
;;; note: source file ./numbers.scm
;;;       newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/numbers/numbers.scm.go
;;; compiling ./numbers.scm
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/numbers/numbers.scm.go
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm.go
%%%% Starting test harness-numbers  (Writing full log to "harness-numbers.log")
# of expected passes      2

Maintenant, je réalise que les deux tests racontent deux histoires ayant un point en commun : un des opérandes est l'élément neutre. Je vais donc faire ressortir ce détail pour éliminer la dupplication.

Modifies le fichier numbers-test.scm :

(use-modules (srfi srfi-64)
             (numbers))

(define DUMMY_NON_NEUTRAL_VALUE 1)

(define (test-add-neutral-element-to value)
  (test-equal (build-test-name value)
    value
    (integer-add value NEUTRAL_ELEMENT)))

(define (build-test-name value)
  (string-append "add-neutral-element-to-" (number->string value)
         "-should-return-" (number->string value)))


(test-begin "harness-numbers")

(test-add-neutral-element-to NEUTRAL_ELEMENT)
(test-add-neutral-element-to DUMMY_NON_NEUTRAL_VALUE)

(test-end "harness-numbers")

Une dernière exécution des tests pour se rassurer et le tour est joué !

$ guile -L . numbers-test.scm 
;;; note: source file /home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm
;;;       newer than compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm.go
;;; note: auto-compilation is enabled, set GUILE_AUTO_COMPILE=0
;;;       or pass the --no-auto-compile argument to disable.
;;; compiling /home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm
;;; compiled /home/jeko/.cache/guile/ccache/3.0-LE-8-4.3/home/jeko/Workspace/guile-handbook/numbers/numbers-test.scm.go
%%%% Starting test harness-numbers  (Writing full log to "harness-numbers.log")
# of expected passes      2

Le détail des résultats montre l'intérêt de la procédure build-test-name :

$ cat harness-numbers.log
%%%% Starting test harness-numbers
Group begin: harness-numbers
Test begin:
  test-name: "add-neutral-element-to-0-should-return-0"
  source-file: "numbers-test.scm"
  source-line: 7
  source-form: (test-equal (build-test-name value) value (integer-add value NEUTRAL_ELEMENT))
Test end:
  result-kind: pass
  actual-value: 0
  expected-value: 0
Test begin:
  test-name: "add-neutral-element-to-1-should-return-1"
  source-file: "numbers-test.scm"
  source-line: 7
  source-form: (test-equal (build-test-name value) value (integer-add value NEUTRAL_ELEMENT))
Test end:
  result-kind: pass
  actual-value: 1
  expected-value: 1
Group end: harness-numbers
# of expected passes      2

Conclusion

  • Plus de pratique du TDD
  • Nombres, addition
  • Retirer les duplications dans le code ET dans les tests