Caratteri

In Guile, un carattere è scritto #\name, dove name è il nome del carattere. Per esempio il carattere a si scrive #\a e lo spazio si scrive #\space.

In questo capitolo creeremo un piccolo programma che determina se la lettera in ingresso appartiene a un intervallo alfabetico. Per esempio: la lettera "P" appartiene all'intervallo [E; Z]? La risposta è sì. La lettera "A" appartiene all'intervallo [O; T] ? La risposta è no.

Scrivere prima il test

;; 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")

Provare a eseguire il test

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

Scrivere la quantità minima di codice per l'esecuzione del test e controllare l'output del test non riuscito

Il terminale dovrebbe restituire un backtrace e il seguente messaggio:

no code for module (characters).

Ora credo che tu abbia capito, devi creare il modulo.

;; characters.scm

(define-module (characters))

Riavvia il test. Dovresti ottenere il seguente feedback:

$ 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

Nel log gestito viene visualizzata l'indicazione (unbound-variable #f "Unbound variable: ~S" (char-belongs) #f). Qui la prossima cosa da fare è definire char-belongs.

;; characters.scm

(define-module (characters))

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

Riavvia il test.

$ 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

Il codice compila. Il test fallisce perché la procedura char-belongs non restituisce #t (ma restituisce #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
  present value: #f
Group end: harness-characters
# of unexpected failures 1

Scrivere abbastanza codice per farlo passare

Qui c'è una sola cosa da fare per superare il test: modificare char-belongs in modo che restituisca #t.

;; characters.scm

(define-module (characters))

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

Riavviare i test:

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

Il test passa! Le modifiche sono sufficienti.

Refactor

Nei test estrarrò una variabile per far intendere che il carattere #\a una lettera dell'alfabeto scelta arbitrariamente.

;; 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")

Naturalmente, affrettati a lanciare i test per convalidare la rifattorizzazione.

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

Avanti!

Scrivere prima il test

;; 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")

Provare a eseguire il test

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

Evviva, fallisce!

$ 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

Scrivere la quantità minima di codice per l'esecuzione del test e controllare l'output del test non riuscito.

Nessun errore di compilazione. La ragione del fallimento è che la procedura char-belongs restituisce costantemente #t. Tuttavia, in questo secondo test, vogliamo che restituisca #f.

is 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
  present value: #f

Scrivere abbastanza codice per farlo passare

Ecco cosa ti propongo:

;; 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))

Se esegui di nuovo i test, noterai che questa modifica ha funzionato!

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

I test scritti finora non richiedono l'uso del secondo parametro. Quindi non mi preoccupo.

Refactor

Nel test harness (in altre parole, la suite di test), estraggo una variabile che contiene l'intervallo dalla lettera 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")

Eseguo i test per confermare che tutto è in ordine. Poi, nel codice testato, rimuovo if che è di troppo.

;; characters.scm

(define-module (characters))

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

Nessuna regressione, tutto è in ordine!

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

Scrivere prima il test

;; 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-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")

Provare a eseguire il 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

È proprio il nostro nuovo test a fallire.

Scrivere la quantità minima di codice per l'esecuzione del test e controllare l'output del test non riuscito.

Nessun errore di compilazione, il test fallisce perché si aspetta il valore #f ma la procedura restituisce #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
  present value: #f

Scrivere abbastanza codice per farlo passare

;; 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))))

Funziona tutto!

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

Refactor

Estraggo la nuova variabile nei test e faccio attenzione a usarla correttamente.

Ho cura di utilizzare un linguaggio cosiddetto commerciale. Pertanto, cambierò il nome della mia procedura da char-belongs a letter-belongs.

Faccio anche attenzione alle convenzioni linguistiche: a ? alla fine di un predicato. La nostra procedura letter-belongs è una di loro.

;; 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")

Estraggo le procedure per rendere più esplicita l'intenzione e per rispettare il principio del Single Layer Abstraction (SLA).

Lo stesso vale per i nomi commerciali e le convenzioni linguistiche.

;; 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))

Si noterà che le procedure preceed-lower-bound? e follow-upper-bound? utilizzano la variabile interval senza bisogno di averla come parametro. Questo perché sono definite localmente nella procedura lettera-belongs?. Si dice che catturano l'ambiente in cui è definita la variabile interval e possono quindi farvi riferimento. Questo si chiama chiusura (closure). È una specificità del paradigma funzionale!

Conclusione

  • Maggiore pratica del TDD
  • Caratteri, confronto
  • Paradigma Funzionale: la chiusura (closure).