the relationship between a class and its clients as a formal agreement, expressing each party's rights and obligations
Defining a function with an associated contract is similar to the way that one defines a regular Clojure function:
(defconstrainedfn sqr "Squares a number" [n] [(number? n) (not= 0 n) ;; requires/domain => (pos? %) (number? %)] ;; ensures/range (* n n))
Simply put this definition states the expectations on the domain and the range of a function, per function arity:
(defn a-function
[args] [DOMAIN => RANGE]
...)
Or to put it another way, it describes what arguments a function requires in order to ensure accuracy:
(defn a-function
[args] [REQUIRES => ENSURES]
...)
Disecting the definition of sqr
you can see how Trammel operates:
sqr
sqr
takes a single argument n
sqr
requires that n
be a number not equal to zerosqr
ensures that it will return a positive numbersqr
performs the action (* n n)
You can see this in action below:
(sqr 5);=> 25
(sqr -5);=> 25
(sqr 0); java.lang.AssertionError: Assert failed:
(not (zero? n))
As you might expect given the contract, the calls to sqr
with 5
and -5
succeed but the call with 0
fails. The example of sqr
is the simplest model for defining a function adhering to a contract. However, you can also define a function allowing different argument arities, as seen below:
(defconstrainedfn doubler "Defines a function that doubles its input." ([n] [(number? n) => (= (* 2 n) %)] (* 2 n))
([x y] [(every? number? [x y]) => (number? %) (= (* 2 (+ x y)) %)] (* 2 (+ x y))))
The form of the contract for doubler
is more involved, but its meaning is still straight-forward:
doubler
that takes either one or two argumentsdoubler
takes a single argument n
doubler
requires that n
be a numberdoubler
ensures that it returns the double of n
– which it does by executing (* 2 n)
doubler
takes two arguments x
and y
doubler
requires that x
and y
both be numbersdoubler
ensures that it returns the double of the sum of x
and y
– which it does by executing (* 2 (+ x y))
You can also use isolated functions in the body of the constraints and the arguments specified in the enclosing arity specification will be passed into it implicitely, as seen below:
(defconstrainedfn sqr "Squares a number" [n] [number? (not= 0 n) => pos? number?] (* n n))
And that’s all there is to it.