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