Contracts programming with Clojure
Clojure Ruby
Switch branches/tags
Nothing to show
Pull request Compare This branch is 27 commits behind fogus:master.
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
docs/release-notes
examples
src/trammel
test/fogus/me
.gitignore
COPYING
README.md
project.clj
thoughts.md
watching.rb

README.md

Trammel

Contracts programming for Clojure.

Example

Function Contracts

    (require '[trammel.provide :as provide])
    
    (defn sqr [n] (* n n))
    
    (sqr 10)
    ;=> 100
    (sqr 0)
    ;=> 0
    
    (provide/contracts 
      [sqr "given a number not equal to zero, sqr ensures that it returns a positive number"
        [x] [number? (not= 0 x) => number? pos?]])
    
    (sqr 10)
    ;=> 100
    (sqr 0)
	; Pre-condition failure: given a number not equal to zero, sqr
    ;   ensures that it returns a positive number
    ; Assert failed: (not= 0 x)

Record Invariants

    (use '[trammel.core :only (defconstrainedrecord)])
    
    (defconstrainedrecord Foo [a b]
	  "Foo record fields are expected to hold only numbers."
      [(every? number? [a b])]
      Object
      (toString [this] (str "record Foo has " a " and " b)))
    
    ;; default ctor with default values
    (->Foo 1 2)
    ;=> #:user.Foo{:a 1, :b 2}
    
    ;; use like any other map/record
    (assoc (->Foo 1 2) :a 88 :c "foo")
    ;=> #:user.Foo{:a 88, :b 2, :c "foo"}
    
    ;; invariants on records checked at runtime    
    (assoc (->Foo 1 2) :a "foo")
	; Pre-condition failure: Foo record fields are expected to hold only numbers.
    ; Assert failed: (every? number? [a b])

Type Invariants

    (use '[trammel.core :only (defconstrainedtype)])
    
    (defconstrainedtype Foo [a b]
	  "Foo type fields are expected to hold only numbers."
      [(every? number? [a b])])
    
    (->Foo 1 2)
    #<Foo user.Foo@73683>
    
    ;; invariants on types checked at constructions time
    (->Foo 1 :b)
    ; Assert failed: (every? number? [a b])

Reference Invariants

    (def a (constrained-atom 0
         "only numbers allowed"
         [number?]))
    
    @a
	;=> 0
    
	(swap! a inc)
	;=> 1
	
    (swap! a str)
	; Pre-condition failure: only numbers allowed 
	
    (compare-and-set! a 0 "a")
	; Pre-condition failure: only numbers allowed 

The same will work on all reference types, including:

  • Refs - Invariants checked in a transaction
  • Agents - Invariants checked on send and send-off, assertion errors handled as normal agent errors
  • Vars - Invariants checked on binding

Getting

Leiningen

Modify your Leiningen dependencies to include Trammel:

    :dependencies [[trammel "0.7.0"] ...]

Maven

Add the following to your pom.xml file:

    <dependency>
      <groupId>trammel</groupId>
      <artifactId>trammel</artifactId>
      <version>0.7.0</version>
    </dependency>

Notes

Trammel is in its infancy but I think that I have a nice springboard for experimentation and expansion, including:

  • Contracts for higher-order functions
  • Better error messages
  • Distinct pre and post exceptions
  • Study the heck out of everything Bertrand Meyer and Walter Bright ever wrote (in progress)
  • defconstraint -- with ability to relax requires and tighten ensures
  • Study the heck out of Racket Scheme (in progress)
  • Modify macros to also allow regular Clojure constraint maps
  • Make the anything constraint cheap (elimination)
  • Allow other stand-alones: true/false, numbers, characters, regexes
  • Make provide-contracts more amenable to REPL use
  • Generate a Foo? function (in progress)
  • Marrying test.generative with Trammel

If you have any ideas or interesting references then I would be happy to discuss at me -the-at-sign- fogus -the-single-period- me.

References

Emacs

Add the following to your .emacs file for better Trammel formatting:

    (eval-after-load 'clojure-mode
      '(define-clojure-indent
         (contract 'defun)
         (defconstrainedfn 'defun)
         (defcontract 'defun)
         (provide 'defun)))

Example REPL Session

Type the following into a REPL session to see how Trammel might be used.

    (defconstrainedtype Bar 
      [a b] 
      [(every? pos? [a b])])
    
    (Bar? (->Bar 1 2))
    
    (defn sqr [n] (* n n))
    
    (provide-contracts
      [sqr "the constraining of sqr" 
        [n] [number? (not= 0 n) => pos? number?]])
    
    (sqr 0)

    (positive-nums -1)

    (type (->Bar))
    
    (.a (->Bar  42 77))
    (.b (->Bar  42 77))
    (.a (->Bar -42 77))
    (.b (->Bar  42 -77))

    (defconstrainedfn sqrt
      [x] [(>= x 0) => (>= % 0)]
      (Math/sqrt x))
    
    (defn- bigger-than-zero? [n] (>= n 0))
    
    (defconstrainedfn sqrt
      [x] [bigger-than-zero? => bigger-than-zero?]
      (Math/sqrt x))
    
    (sqrt 10)
    (sqrt -19)
    
    (defconstrainedfn sqrt
      [x] [bigger-than-zero? => bigger-than-zero? (<= (Math/abs (- x (* % %))) 0.01)]
      (Math/sqrt x))
    
    (* (sqrt 30) (sqrt 30))
	
    (def ag (constrained-agent 0
             "only numbers allowed"
             [number?]))
    
    (send ag str)
    
    @ag
    
    (agent-error ag)
    
    (def r (constrained-ref 0
             "only numbers allowed"
             [number?]))
    
    (dosync (alter r inc))
    
    (dosync (alter r str))
    
    (def a (constrained-atom 0
             "only numbers allowed"
             [number?]))
    
    @a
    
    (swap! a inc)
    
    (swap! a str)
    (compare-and-set! a 0 "a")
    
    (defconstrainedvar ^:dynamic foo 0
      "only numbers allowed in Var foo"
      [number?])
    
    (binding [foo :a] [foo])