Characters

In Guile, a character is written #\name, where name is the name of the character. For example the character a is written #\a and space is written #\space.

In this chapter we will create a small program that determines whether the input letter belongs to an alphabetical range. For example: does the letter P belong to the interval [E; Z]? The answer is yes. Does the letter A belong to the interval [O; T] ? The answer is no.

Write the test first

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

Try and run the test

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

Write the minimal amount of code for the test to run and check the failing test output

The terminal should return a backtrace and the following message:

no code for module (characters).

Now I think you get it, you have to create the module.

;; characters.scm

(define-module (characters))

Restart the test. You should get the following 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

In the managed log, you will see the indication (unbound-variable #f "Unbound variable: ~S" (char-belongs) #f). So the next thing to do is to define char-belongs.

;; characters.scm

(define-module (characters))

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

Restart the 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

The code compiles. The test fails because the char-belongs procedure does not return #t (it returns #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

Write enough code to make it pass

Here you have one thing to do to pass the test: modify char-belongs to return #t.

;; characters.scm

(define-module (characters))

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

Restart the tests :

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

The test passes! The modification was enough.

Refactor

Here, I will extract a variable in the tests to carry the intention that the character #\a is a letter of the alphabet chosen arbitrarily.

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

Of course, you hurry to launch the tests to validate the refactoring.

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

Next!

Write the test first

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

Try and run the test

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

Yippee, it's a failure!

$ 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

Write the minimal amount of code for the test to run and check the failing test output

No compilation error. The reason for the failure is that the char-belongs procedure constantly returns #t. However, in this second test, we want it to return #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

Write enough code to make it pass

Here's what I propose to you:

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

If you run the tests again, you'll see that this change does the trick!

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

The tests written so far do not require the use of the second parameter. So I don't bother.

Refactor

In the test harness (in other words, the test suite), I extract a variable that contains the range from the letter 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")

Run the tests to confirm that everything is in order. Then, in the tested code, I remove the if that is too much.

;; characters.scm

(define-module (characters))

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

No regression, everything is in order!

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

Write the test first

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

Try and run the 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

It is indeed our new test that fails.

Write the minimal amount of code for the test to run and check the failing test output

No compilation error, the test fails because we wait for the value #f but the procedure returns #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

Write enough code to make it pass

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

Everything passes!

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

Refactor

I extract the new variable in the tests and I take care to use them properly.

I take care to use a, so-called, business language. I will therefore change the name of my procedure from char-belongs to letter-belongs.

I also pay attention to the language conventions: a ? at the end of a predicate. Our letter-belongs procedure is one of them.

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

I extract procedures to make the intention more explicit and to respect the principle of Single Layer Abstraction (SLA).

Same for business names and language conventions.

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

You will notice that the preceed-lower-bound? and follow-upper-bound? procedures use the interval variable without needing to have it as a parameter. This is because they are defined locally in the letter-belongs? procedure. It is said that they capture the environment where the interval variable is defined and can then refer to it. This is called a closure. It is a specificity of the functional paradigm!

Conclusion

  • More TDD practice
  • Characters, comparison
  • Functional Paradigm: the closure.