Numbers

Numbers are a relatively basic type of data. Guile implements a Tower of Numerical Types, offering the hacker a plethora of number types with their own set of features.

Here, I'll focus on integers and their addition, but feel free to experiment with what you need. The goal of this chapter will be to create a partially implemented `integer-add' procedure and see how it all works.

Write the test first

So, in your workspace, create a directory ~/Workspace/guile-handbook/numbers where you will edit the file numbers-test.scm. Below, the first test has been added. This is the case that seems to me to be the simplest.

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

(test-begin "harness-numbers")

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

(test-end "harness-numbers")

You can notice that I use the numbers module, it will contain the integer-add function.

Try and run the test

$ guile -L . numbers-test.scm

The compilateur complains :

no code for module (numbers)

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

Now create the numbers module in the ~/Workspace/guile-handbook/numbers/numbers.scm file to satisfy the compiler and nothing else!

(define-module (numbers))

Reload the tests. Then you can see another compilation error:

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

Repeat the process and, each time, let the compiler guide you until you see your test fail for the right reason. The file ~/Workspace/guile-handbook/numbers/numbers.scm becomes:

(define-module (numbers))

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

Now we can see that the test fails. As you can see in the test report : the expected value is 0 but the value we got is -1 (yes, it is done on purpose!).

%%%% 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

Write enough code to make it pass

You will now change the minimum code to pass the test. Watch your eyes:

(define-module (numbers))

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

Running the test proves that this modification is sufficient. It's time for the…

Refactor

There is little code here, but still something remarkable: the value zero is hard-coded and left as is. However, in our application, it has a special meaning, we say a business meaning. Indeed, for the addition, zero is the neutral element. I will therefore make this information explicit.

(define-module (numbers))

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

This variable is locally defined in the integer-add procedure, its only use.

Ok, let's start another TDD cycle!

Write the test first

New test in 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")

Try and run the test

Run the test and inspect the error.

$ guile -L . numbers-test.scm

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

No errors at compilation time. The test fails correctly!

[…]
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

Write enough code to make it pass

The difference between the first and second test is the value of the first parameter given to integer-add. Here's what I propose: the returned value depends on the value of this first parameter.

(define-module (numbers))

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

Run the tests again.

$ guile -L . numbers-test.scm

All green!

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

Refactor

In this second test, I used the value 1 arbitrarily. I could have chosen another value, as long as it is not the neutral element of the addition. I will therefor make this intent explicit in the name of a variable that will have this value.

Here is 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")

After each small modification during the refactoring, it is recommended to run the tests to make sure that everything is in order.

$ 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

We're good. Now I realize that the NEUTRAL_ELEMENT variable would be useful in testing as well.

Here is numbers.scm:

(define-module (numbers))

(define-public NEUTRAL_ELEMENT 0)

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

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

Make sure you haven't broken any 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

Now I realize that the two tests tell two stories with one thing in common: one of the operands is the neutral element. So I'm going to make it explicit. As a result, it will eliminate the duplication.

See the new 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")

One last test execution:

$ 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

The detail of the results shows the interest of the build-test-name procedure:

$ 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

Wrapping up

  • More practice of the TDD workflow
  • Numbers, addition
  • Remove duplicates in the code AND in the tests