Testing your code and adding it to the Unison namespace
Let's add a test for our
use .base square : Nat -> Nat square x = x * x use test.v1 test> tests.square.ex1 = run (expect (square 4 == 16))
Save the file, and Unison comes back with:
7 | test> tests.square.ex1 = run (expect (square 4 == 16)) ✅ Passed : Passed 1 tests.
Some syntax notes:
test>prefix tells Unison that what follows is a test watch expression. Note that we're also giving a name to this expression,
expect function has type
Boolean -> Test. It takes a
Boolean expression and gives back a
Test, which can be
run to produce a list of test results, of type
view base.Test.Result). In this case there was only one result, and it was a passed test.
A property-based test
Let's test this a bit more thoroughly.
square should have the property that
square a * square b == square (a * b) for all choices of
b. The testing library supports writing property-based tests like this. There's some new syntax here, explained afterwards:
use .base square : Nat -> Nat square x = x * x use test.v1 test> tests.square.ex1 = run (expect (square 4 == 16)) test> tests.square.prop1 = go _ = a = !nat b = !nat expect (square a * square b == square (a * b)) runs 100 go
8 | go _ = a = !nat ✅ Passed : Passed 100 tests. (cached)
This will test our function with a bunch of different inputs.
- The Unison block which begins after an
=begins a Unison block, which can have any number of bindings (like
a = ...) all at the same indentation level, terminated by a single expression (here
expect (square ..)), which is the result of the block.
- You can call a function parameter
_if you just plan to ignore it. Here,
goignores its argument; its purpose is just to make
golazily evaluated so it can be run multiple times by the
!exprmeans the same thing as
expr (), we say that
!exprforces the delayed computation
- Note: there's nothing special about the names
tests.square.prop1; we could call those bindings anything we wanted. Here we just picked some uncreative names based on the function being tested. Use whatever naming convention you prefer.
nat comes from
test.v1.nat. It's a generator of natural numbers.
!nat generates one of these numbers.
square function and the tests we've written for it are not yet part of the codebase. So far they only exists in our scratch file. Let's add it now. Switch to the Unison console and type
add. You should get something like:
.> add ⍟ I've added these definitions: tests.square.ex1 : [base.Test.Result] tests.square.prop1 : [base.Test.Result] square : base.Nat -> base.Nat
You've just added a new function and some tests to your Unison codebase. Try typing
view square or
view tests.square.prop1. Notice that Unison inserts precise
use statements when rendering your code.
use statements aren't part of your code once it's in the codebase. When rendering code, a minimal set of
use statements is inserted automatically by the code printer, so you don't have to be precise with your
If you type
test at the Unison prompt, it will "run" your test suite:
.> test Cached test results (`help testcache` to learn more) ◉ tests.square.ex1 : Passed 1 tests. ◉ tests.square.prop1 : Passed 100 tests. ✅ 2 test(s) passing Tip: Use view tests.square.ex1 to view the source of a test.
But actually, it didn't need to run anything! All the tests had been run previously and cached according to their Unison hash. In a purely functional language like Unison, tests like these are deterministic and can be cached and never run again. No more running the same tests over and over again!
Unison namespaces and use clauses
Now that we've added our
square function to the codebase, how do we reference it elsewhere?
The Unison namespace is the mapping from names to definitions. Names in Unison look like this:
foo. That is: an optional
., followed by one or more segments separated by a
., with the last segment allowed to be an operator name like
We often think of these names as forming a tree, much like a directory of files, and names are like file paths in this tree. Absolute names (like
.base.Int) start with a
. and are paths from the root of this tree and relative names (like
math.sqrt) are paths starting from the current namespace, which you can set using the
namespace (or equivalently
.> namespace mylibrary ☝️ The namespace .mylibrary is empty. .mylibrary>
Notice the prompt changes to
.mylibrary>, indicating your current namespace is now
.mylibrary. When editing scratch files, any relative names not locally bound in your file will be resolved by prefixing them with the current namespace of
.mylibrary. And when you issue an
add command, the definitions are put directly into this namespace. For instance, if we added
x = 42 to our scratch file and then did
.mylibrary> add, that would create the definition
You can use
namespace .to move back to the root.
When we added
square, we were at the root, so
square and its tests are directly under the root. To keep our root namespace a bit tidier, let's go ahead and move our definitions into the
.mylibrary> move.term .square square Done. .mylibrary> find 1. square : .base.Nat -> .base.Nat .mylibrary> move.namespace .tests tests Done.
.square to refer to the
square definition directly under the root, and then moving it to the relative name
square. When you're done shuffling some things around, you can use
find with no arguments to view all the definitions under the current namespace:
.mylibrary> find 1. tests.square.ex1 : [.base.Test.Result] 2. tests.square.prop1 : [.base.Test.Result] 3. square : .base.Nat -> .base.Nat
Also notice that we don't need to rerun our tests after this reshuffling. The tests are still cached:
.mylibrary> test Cached test results (`help testcache` to learn more) ◉ tests.square.ex1 : Passed 1 tests. ◉ tests.square.prop1 : Passed 100 tests. ✅ 2 test(s) passing Tip: Use view tests.square.ex1 to view the source of a test.
We get this for free because the test cache is keyed by the hash of the test, not by what the test is called.
usestatement can do absolute names as well, for instance
use .base.List map.
When you're starting out writing some code, it can be nice to just put it in a temporary namespace, perhaps called
scratch. Later, without breaking anything, you can move that namespace or bits and pieces of it elsewhere, using the
Next: modifying definitions