Caractères

En Guile, un caractère s'écrit #\name, où name est le nom du caractère. Par exemple le caractère a s'écrit #\a et l'espace s'écrit #\space.

Dans ce chapitre, nous allons créer un petit programme qui détermine si la lettre donnée en entrée appartient à un intervalle alphabétique. Par exemple : est-ce que la lettre P appartient à l'intervalle [E ; Z] ? La réponse est oui. Est-ce que la lettre A appartient à l'intervalle [O ; T] ? La réponse est non.

Écrire le test d’abord

;; characters-test.scm

(use-modules (srfi srfi-64)
             (characters))

(test-begin "harness-characters")

(test-assert "a char belongs to its own interval"
  (char-belongs #\a (cons #\a #\a)))

(test-end "harness-characters")

Essayer et lancer le test

$ guile --no-auto-compile -L . characters-test.scm

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

Le terminal devrait te retourner une backtrace et le message suivant :

no code for module (characters)

A présent, je pense que tu as compris, tu dois créer le module.

;; characters.scm

(define-module (characters))

Relances le test. Tu devrais obtenir le retour suivant :

$ guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
characters-test.scm:6: FAIL a char belongs to its own interal
# of unexpected failures  1

Dans le log géréré, tu verras l'indication (unbound-variable #f "Unbound variable: ~S" (char-belongs) #f). La prochaine chose à faire est donc de définir char-belongs.

;; characters.scm

(define-module (characters))

(define-public (char-belongs char interval)
  "Return true if char belongs to interval, else return false."
  #f)

Relances les tests.

$ guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
characters-test.scm:6: FAIL a char belongs to its own interal
# of unexpected failures  1

Le code compile. Le test échoue car la procédure char-belongs ne retourne pas #t (elle retourne #f).

$ cat harness-characters.log 
%%%% Starting test harness-characters
Group begin: harness-characters
Test begin:
  test-name: "a char belongs to its own interal"
  source-file: "characters-test.scm"
  source-line: 6
  source-form: (test-assert "a char belongs to its own interal" (char-belongs #\a (list #\a #\a)))
Test end:
  result-kind: fail
  actual-value: #f
Group end: harness-characters
# of unexpected failures  1

Écrire juste assez de code pour faire passer le test

Tu as ici une chose à faire pour faire passer le test : modifier char-belongs pour retourner #t.

;; characters.scm

(define-module (characters))

(define-public (char-belongs char interval)
  "Return true if char belongs to interval, else return false."
  #t)

Relances les tests :

 guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
# of expected passes      1

Le test passe ! La modification a suffit.

Réusiner

Ici, je vais extraire une variable dans les tests pour porter l'intention que le caractère #\a est une lettre de l'alphabet choisie arbitrairement.

;; characters-test.scm

(use-modules (srfi srfi-64)
             (characters))

(test-begin "harness-characters")

(define DUMMY_LETTER #\a)

(test-assert "a char belongs to its own interval"
  (char-belongs DUMMY_LETTER (cons DUMMY_LETTER DUMMY_LETTER)))

(test-end "harness-characters")

Bien sûr, tu t'empresses de lancer les tests pour valider le réusinage.

$ guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
# of expected passes      1

Au suivant !

Écrire le test d’abord

;; characters-test.scm

(use-modules (srfi srfi-64)
             (characters))

(test-begin "harness-characters")

(define DUMMY_LETTER_1 #\a)
(define DUMMY_LETTER_2 #\b)

(test-assert "a char belongs to its own interval"
  (char-belongs DUMMY_LETTER_1 (cons DUMMY_LETTER_1 DUMMY_LETTER_1)))

(test-assert "a char does not belong to another char interval"
  (not (char-belongs DUMMY_LETTER_2 (cons DUMMY_LETTER_1 DUMMY_LETTER_1))))

(test-end "harness-characters")

Essayer et lancer le test

$ guile --no-auto-compile -L . characters-test.scm

Youpi, c'est un échec !

$ guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
characters-test.scm:12: FAIL a char does not belong to another char interval
# of expected passes      1
# of unexpected failures  1

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

Pas d'erreur de compilation. La raison de l'échec est que la procédure char-belongs retourne constamment #t. Or, dans ce second test, on souhaite qu'elle renvoit #f.

est begin:
  test-name: "a char does not belong to another char interval"
  source-file: "characters-test.scm"
  source-line: 12
  source-form: (test-assert "a char does not belong to another char interval" (not (char-belongs DUMMY_LETTER_2 (cons DUMMY_LETTER_1 DUMMY_LETTER_1))))
Test end:
  result-kind: fail
  actual-value: #f

Écrire juste assez de code pour faire passer le test

Voilà ce que je te propose :

;; characters.scm

(define-module (characters))

(define-public (char-belongs char interval)
  "Return true if char belongs to interval, else return false."
  (if (char=? char #\a)
      #t
      #f))

Si tu relances les tests, tu constate que cette modification fait l'affaire !

$ guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
# of expected passes      2

Les tests écris jusqu'à maintenant ne nécessice pas d'utiliser le second paramètre. Donc je ne me donne pas cette peine.

Réusiner

Dans le harnais de tests (autrement dit, la suite de tests), j'extrais une variable qui contient l'intervalle de la lettre a.

;; characters-test.scm

(use-modules (srfi srfi-64)
             (characters))

(test-begin "harness-characters")

(define DUMMY_LETTER_1 #\a)
(define DUMMY_LETTER_2 #\b)

(define INTERVAL_DUMMY_LETTER_1 (cons DUMMY_LETTER_1 DUMMY_LETTER_1))

(test-assert "a char belongs to its own interval"
  (char-belongs DUMMY_LETTER_1 INTERVAL_DUMMY_LETTER_1))

(test-assert "a char does not belong to another char interval"
  (not (char-belongs DUMMY_LETTER_2 INTERVAL_DUMMY_LETTER_1)))

(test-end "harness-characters"

Lances les tests pour confirmer que tout est en ordre. Puis, dans le code testé, je retire le if qui est de trop.

;; characters.scm

(define-module (characters))

(define-public (char-belongs char interval)
  "Return true if char belongs to interval, else return false."
  (char=? char #\a))

Pas de régression, tout est en ordre !

$ guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
# of expected passes      2

Écrire le test d’abord

(use-modules (srfi srfi-64)
             (characters))

(test-begin "harness-characters")

(define DUMMY_LETTER_1 #\a)
(define DUMMY_LETTER_2 #\b)

(define INTERVAL_DUMMY_LETTER_1 (cons DUMMY_LETTER_1 DUMMY_LETTER_1))

(test-assert "a char belongs to its own interval"
  (char-belongs DUMMY_LETTER_1 INTERVAL_DUMMY_LETTER_1))

(test-assert "a char does not belong to another char interval"
  (not (char-belongs DUMMY_LETTER_2 INTERVAL_DUMMY_LETTER_1)))

(test-assert "a letter preceding the lower bound of the interval does not belong to it"
  (not (char-belongs DUMMY_LETTER_1 (cons #\b #\c))))

(test-end "harness-characters")

Essayer et lancer le test

$ guile --no-auto-compile -L . characters-test.scm

$ guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
characters-test.scm:17: FAIL a letter preceding the lower bound of the interval does not belong to it
# of expected passes      2
# of unexpected failures  1

C'est bien notre nouveau test qui échoue.

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

Pas d'erreur de compilaton, le test échoue bien car on attend la valeur #f mais la procédure renvoit #t.

Test begin:
  test-name: "a letter preceding the lower bound of the interval does not belong to it"
  source-file: "characters-test.scm"
  source-line: 17
  source-form: (test-assert "a letter preceding the lower bound of the interval does not belong to it" (not (char-belongs DUMMY_LETTER_1 (cons #\b #\c))))
Test end:
  result-kind: fail
  actual-value: #f

Écrire juste assez de code pour faire passer le test

;; characters.scm

(define-module (characters))

(define-public (char-belongs char interval)
  "Return true if char belongs to interval, else return false."
  (and (char<=? char (car interval)) (char>=? char (cdr interval))))

Tout passe !

$ guile --no-auto-compile -L . characters-test.scm 
%%%% Starting test harness-characters  (Writing full log to "harness-characters.log")
# of expected passes      3

Réusiner

J'extraie la nouvelle variable dans les tests et je prends soin de bien les utiliser.
Je fais attention d'utiliser un langage dit métier, cohérent. Je vais de ce fait changer le nom de ma procédure char-belongs en letter-belongs.
Je fais aussi attention aux conventions de langage : un ? à la fin d'un prédicat. Notre procédure letter-belongs en est un.

;; characters-test.scm

(use-modules (srfi srfi-64)
             (characters))

(test-begin "harness-characters")

(define DUMMY_LETTER_1 #\a)
(define DUMMY_LETTER_2 #\b)
(define DUMMY_LETTER_3 #\c)

(define INTERVAL_DUMMY_LETTER_1 (cons DUMMY_LETTER_1 DUMMY_LETTER_1))

(test-assert "a letter belongs to its own interval"
  (letter-belongs? DUMMY_LETTER_1 INTERVAL_DUMMY_LETTER_1))

(test-assert "a letter does not belong to another letter's interval"
  (not (letter-belongs? DUMMY_LETTER_2 INTERVAL_DUMMY_LETTER_1)))

(test-equal "a letter preceding the lower bound of the interval does not belong to it"
  #f
  (letter-belongs? DUMMY_LETTER_1 (cons DUMMY_LETTER_2 DUMMY_LETTER_3)))

(test-end "harness-characters")

J'extraie des procédures pour rendre plus explicite l'intention et respecter le principe du Single Layer Abstraction (SLA).
Idem pour les appellations métier et les conventions de langages.

;; characters.scm

(define-module (characters))

(define-public (letter-belongs? letter interval)
  "Return true if letter belongs to interval, else return false."

  (define (preceed-lower-bound? letter)
    (char<=? letter (car interval)))

  (define (follow-upper-bound? letter)
    (char>=? letter (cdr interval)))
  
  (and (preceed-lower-bound? letter) (follow-upper-bound? letter))

Tu remarqueras que les procédures preceed-lower-bound? et follow-upper-bound? utilisent la variable interval sans avoir besoin de l'avoir en paramètre. C'est parce qu'elles sont définies localement dans la procédure letter-belongs?. On dit qu'elles capturent cet environnement où la variable interval est définie et peuvent alors s'y référer. On appelle ça une closure. C'est une spécificité du paradigme fonctionnel !

Conclusion

  • Plus de pratique du TDD
  • Charactères, comparaison
  • Paradigme fonctionnel : la closure