A little sugar with your Clojure Aspect Contracts
TL;DR: clojure-contracts-sugar - some sugar macros for clojure.core.contracts
Introduction
Back in November 2012, in one of my first toe-dippings into Clojure, I wrote a post on my initial experiences with Michael Fogus’s clojure.core.contracts (hereafter just CCC).
The rest of this post assumes you have a passing familiarity with CCC. If you aren’t, you may find my original post a digestible introduction.
Although using CCC for contracts programming is self-evident, it was the opportunity to use the Clojure’s :pre and :post assertions to support the concept and potential of aspects as defined originally by Gregor Kiczales in his work on Aspect Oriented Programming (AOP).
In this post I’m following through the experiments and ideas of the original post, and illustrating with some examples using a new library I’ve written recently - clojure-contracts-sugar (CHUGAR).
In the original post I made the point that CCC could do with some productivity aids - sugar - to ease the rich usage of CCC. CHUGAR attempts to supply that sugar and you can think of this post as partly a getting started tutorial for the library.
In the simplest cases, CHUGAR reduces to CCC albeit with a slightly more flexible syntax. If your contracts needs are straightforward, you’re likely better off using CCC or even :pre and :post assertions directly.
That said, I would encourage you though to have a look at the sections on mnemonics below which offer an easy way to use (and re-use) rich and flexible aspect contracts.
Note, the usual caveat, CHUGAR is a work in progress and really is intended to be only a starting point (foundation) for explorations of contracts from Clojure. The API is “settling” but may change.
A quick note on terminology: in the original post I used suck to identify the (input) arguments of a function and spit the result (return value). That “convention” is used extensively both here and in the library’s code.
Aspect Oriented Programming
I first bumped into AOP reading the Communications of the ACM’s October 2001 edition special edition on AOP (paywall for content). In their introductory article in that volume, Elrad, Filman and Bader explains the need for AOP like so:
Any structural realization of a system will find that some concerns are neatly localized within a specific structural piece, while others cross multiple elements. AOP is focused on mechanisms for simplifying the realization of such crosscutting concerns.
and
Separating the expressions of multiple concerns in programming systems promises simpler system evolution, more comprehensible systems, adaptability, customizability, and easier reuse.
They go on to highlight the opportunity for a services paradigm for common requirements such as authentication, logging, etc facilitating clear separation of concerns: Leaving the application developers to focus of their agenda whilst minimising the potential for “misuse”, whether by omission or commission, of the specialist subsystems e.g. authentication which may be supported by other, third, parties:
Implicit invocation is a virtue in this age of increased software complexity, as domain experts for an application are unlikely to be familiar with intricacies of specialized algorithms for distribution, authentication, access control, synchronization, encryption, redundancy, and so forth, and cannot be trusted to always invoke them appropriately in their programs.
In Ramnivas Laddad’s book on AspectJ, he says:
AOP is a new methodology that provides separation of crosscutting concerns by introducing a new unit of modularization—an aspect—that crosscuts other modules. With AOP you implement crosscutting concerns in aspects instead of fusing them in the core modules.
The result is that AOP modularizes the cross-cutting concerns in a clear-cut fashion, yielding a system architecture that is easier to design, implement, and maintain.
In another take, in Emerick, Carper and Grand’s book Clojure Programming, they say:
Aspect-oriented programming (AOP) is a methodology that allows separation of cross-cutting concerns. In object-oriented code, a behavior or process is often repeated in multiple classes, or spread across multiple methods. AOP is a way to abstract this behavior and apply it to classes and methods without using inheritance.
The Code
Jar is on Clojars
The jar is on Clojars:
Leiningen dependency information:
1
[name.rumford/clojure-contracts-sugar "0.2.0"]
Maven dependency information:
1
2
3
4
5
<dependency>
<groupId>name.rumford</groupId>
<artifactId>clojure-contracts-sugar</artifactId>
<version>0.2.0</version>
</dependency>
Repo is on Github
The repo is on github. As you might expect, its organised as a Leiningen project so you’ll want Leiningen installed.
The project structure is Maven-style but there is only Clojure today: ./src/main/clojure and ./src/test/clojure.
The code uses another of my other new libraries clojure-carp for some utility functions, exceptions, diagnostics and other miscellany.
Documentation
The repo’s ./doc folder contains the source of this post: its an emacs org file tangled to generate the examples below in a Leiningen project.
The folder also contains an (org and html) file code-notes offering a brief high-level overview of the main code artefacts.
Tests
There are a number of tests providing reasonable code coverage that can be run from the repo:
1
lein test aspect-tests1
Examples
The examples below can be found in the repo’s examples folder (specifically in ./examples/aspect_examples) and they can be run using lein in the usual way:
1
2
3
cd ./examples/aspect-examples
lein deps
lein run -m aspect-examples1
The examples use a couple of harness functions - will-work and will-fail - to run tests.
will-work takes as arguments the constrained function and a list of arguments.
will-fail similarly takes just the constrained function and its arguments and catches the AssertionError expected to be thrown.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
;; Helper for accessor examples expected to work. Returns the expected result, else fails
(defn will-work
[fn-constrained & fn-args]
(let [actual-result (apply fn-constrained fn-args)]
(println "will-work" "worked as expected" "actual-result" actual-result "fn-constrained" fn-constrained "fn-args" fn-args)
actual-result))
;; Helper for accessor examples expected to fail. Catches the expected AssertionError, else fails.
;; A nil return from the function is ok
(defn will-fail
[fn-constrained & fn-args]
(try
(do
(let [return-value (apply fn-constrained fn-args)]
(if return-value (assert (println "will-fail" "DID NOT FAIL" "did not cause AssertionError" "fn-constrained" fn-constrained "fn-args" fn-args "RETURN-VALUE" (class return-value) return-value)))))
(catch AssertionError e
(println "will-fail" "failed as expected" "fn-constrained" fn-constrained "fn-args" fn-args))))
Using Contract Aspects - Apply v Update
The libary has two main aspect contract macros: apply-contract-aspects and update-contract-aspects.
The majority of examples below use apply-contract-aspects but update-contract-aspects could be used just as well.
A couple of very simple examples follow to give a flavour of their usage with the details expanded upon in the following sections.
Using apply-contract-aspects
The first macro, apply-contract-aspects, applies one or more aspects to an existing function and returns a new function.
Example - applying a built-in predicate
The below will create, from the original function any-fn, a new constrained function map-fn that will only suck a map as its input argument. (The return value will be unconstrained.)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
;; Example - applying a built-in predicate
;; any-fn is the "base" function
(defn any-fn [x] x)
;; map-fn is the new function constrained to suck a map
(def suck-map-fn1 (apply-contract-aspects any-fn map?))
;; This will work
(will-work suck-map-fn1 {:a 1 :b 2 :c 3})
;; But this will fail since suck-map-fn1 can only suck a map
(will-fail suck-map-fn1 [1 2 3])
;; The original function any-fn is unchanged and not constrained in any way
(will-work any-fn {:a 1 :b 2 :c 3})
(will-work any-fn [1 2 3])
(will-work any-fn :a)
(will-work any-fn 99)
The map? predicate in the above call to apply-contract-aspects is the Contract Definition.
Under the covers, apply-contract-aspects generates a CCC contract similar to the below where the ctx-aspect2721 is the random, but unique, name (gensym) of the contract function.
1
2
;; Example - example of the generated clojure.core.contract call
(clojure.core.contracts/contract ctx-aspect2721 "\"ctx-aspect2721\"" [arg0] [(map? arg0)])
Quick note on argument names: the arguments in a generated contract are given names arg0, arg1, etc. These names can be used to refer explicitly to specific arguments. More on this later.
Similarly, to suck a vector:
1
2
3
4
5
6
7
;; Example - suck a vector
(def suck-vector-fn1 (apply-contract-aspects (fn [x] x) vector?))
(will-work suck-vector-fn1 [1 2 3])
(will-fail suck-vector-fn1 99)
Example - applying your own custom predicate
You can of course create and use your own custom predicate function, returning true or false as decided. You can constrain multiple input arguments and/or the return value in a custom predicate.
A simple way to create a custom predicate would be to use :pre and post assertions in an “identity” function.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
;; Example - applying your own custom predicate
;; The custom predicate ensures the argument is a map, its keys are keywords and values are numbers.
(defn is-map-with-keyword-keys-and-numeric-values?
[x]
{:pre [(map? x) (every? keyword? (keys x)) (every? number? (vals x))]}
x)
(def map-keyword-keys-numeric-values-fn1 (apply-contract-aspects any-fn is-map-with-keyword-keys-and-numeric-values?))
;; This will work
(will-work map-keyword-keys-numeric-values-fn1 {:a 1 :b 2 :c 3})
;; But these will fail the contracts
(will-fail map-keyword-keys-numeric-values-fn1 {:a :x :b 2 :c 3})
(will-fail map-keyword-keys-numeric-values-fn1 {"x" 1 :b 2 :c 3})
(will-fail map-keyword-keys-numeric-values-fn1 [1 2 3])
;; As before the original function any-fn is unchanged and not constrained in any way
(will-work any-fn {:a 1 :b 2 :c 3})
(will-work any-fn [1 2 3])
(will-work any-fn :a)
(will-work any-fn 99)
Using update-contract-aspects
The second macro, update-contract-aspects, “changes” (using alter-var-root) an existing function.
Example - updating a function with a built-in predicate
Essentially the same example as above except the source function but any-fn is changed to only suck a map.
1
2
3
4
5
6
7
8
9
10
11
12
13
;; Example - updating a function with a built-in predicate
;; any-fn is "changed" to now only suck a map
(update-contract-aspects any-fn map?)
;; This will work
(will-work any-fn {:a 1 :b 2 :c 3})
;; But this will fail as any-fn can now only suck a map
(will-fail any-fn [1 2 3])
Applying Contracts to Many Arguments and the Result
Many functions will have more than one (suck) argument, even different arities, each likely requiring its own specific assertions (constraints), and the (spit) result maybe different assertion(s) again.
To support a rich definition of the assertions required by each argument and the return value, the contract definition can be specified as a map with two keys: :suck and :spit where the value of the keys are the assertions to apply to the input arguments and return values. An example should clarify.
Example - suck a map and keyword and spit a vector
The below defines a two argument contract: the first argument must be a map, the second a keyword; with a vector expected as the result:
1
{:suck [map? keyword?] :spit vector?}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;; Example - suck a map and keyword and spit a vector
;; In this example, the assertion constrains the function to suck a map and keyword
;; and spit a vector.
;; The function looks up the value of the keyword in the map.
(def suck-map-keyword-spit-vector-fn1 (apply-contract-aspects (fn [m k] (k m)) {:suck [map? keyword?] :spit vector?}))
;; This will work as key :c contains a vector
(will-work suck-map-keyword-spit-vector-fn1 {:a 1 :b 2 :c [1 2 3]} :c)
;; But these will fail
(will-fail suck-map-keyword-spit-vector-fn1 {:a 1 :b 2 :c 3} :c)
(will-fail suck-map-keyword-spit-vector-fn1 {:a 1 :b 2 :c 3} :d)
Some notes:
- assertions are matched positionally to their arguments
The map? constrains only the first argument (arg0) and the keyword? constrains only the second argument (arg1); the returned value must be a vector?.
- if there is only one argument, the enclosing vector is not needed
Just as the return value can be specified as just vector? and not [vector?], if the function only sucked a map :suck map? would be sufficient e.g. {:suck map? :spit vector?}.
Example - suck a map - with keyword keys and numeric values - and keyword and spit a vector
To include additional assertions on the map in the previous example to insist on keyword keys and numeric values, the assertion for the map argument would be changed to a vector of constraints.
Note the use of arg0 to refer to the input map in the every? clauses.
1
{:suck [[map? (every? keyword? (keys arg0)) (every? number? (vals arg0))] keyword?] :spit vector?}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;; Example - suck a map - with keyword keys and numeric values - and keyword and spit a vector
;; In this example, the contract constrains the function to suck a map and keyword, spit a number.
;; The map must have keywords keys and numeric values.
(def suck-map-keyword-spit-number-fn1 (apply-contract-aspects (fn [m k] (k m)) {:suck [[map? (every? keyword? (keys arg0)) (every? number? (vals arg0))] keyword?] :spit number?}))
;; This will work
(will-work suck-map-keyword-spit-number-fn1 {:a 1 :b 2 :c 3} :a)
;; But these will fail their contracts
(will-fail suck-map-keyword-spit-number-fn1 {:a :x :b 2 :c 3} :a)
(will-fail suck-map-keyword-spit-number-fn1 {:a 1 :b 2 :c 3} :d)
(will-fail suck-map-keyword-spit-number-fn1 {"x" 1 :b 2 :c 3} :c)
Example - specifying argument order explicitly
Specifying the arguments’ order implicitly by their position in the suck assertion list is natural but there may be times when you want to explicitly define the argument position and its assertions, irrespective of its position in the assertion list.
You can do this by providing a map where the keys are the argument positions and the values the assertion list to apply to that argument.
The example below is a variant of the map and keyword example above but the keyword is the first argument (key 0) and the map the second (key 1). The map must have keyword keys and numeric values as before.
1
{:suck {0 :keyword 1 [:map (every? keyword? (keys arg0)) (every? number? (vals arg0))]} :spit :number}
Note the use of arg0 to refer to the input map in the every? clauses even though the map is the second argument (and will therefore be arg1 in the contract).
That’s because the every? forms will be rewritten automatically to reflect the map’s position in the argument order i.e. its arg1. The point is that the map assertion does not change no matter where the map appears in the argument order.
This is similar to when mnemonics are composed - see later.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;; Example - specifying argument order explicitly
;; In this example, the arguments are specified by their explicit position in the argument order
(def explicit-argument-order-fn1 (apply-contract-aspects (fn [k m] (k m)) {:suck {0 :keyword 1 [:map (every? keyword? (keys arg0)) (every? number? (vals arg0))]} :spit :number}))
;; This will work
(will-work explicit-argument-order-fn1 :a {:a 1 :b 2 :c 3})
;; But these will fail their contracts
(will-fail explicit-argument-order-fn1 :a {:a :x :b 2 :c 3})
(will-fail explicit-argument-order-fn1 :d {:a 1 :b 2 :c 3})
(will-fail explicit-argument-order-fn1 :c {"x" 1 :b 2 :c 3})
BTW The contract looks like this. Note the map is arg1.
1
(clojure.core.contracts/contract ctx-aspect3000 "\"ctx-aspect3000\"" [arg0 arg1] [(keyword? arg0) (map? arg1) (every? keyword? (keys arg1)) (every? number? (vals arg1)) => (number? %)])
Using CCC’s contract definition form
For those familiar with CCC, you can also use CCC’s contract specification format as well. But note the signature vector (e.g. ‘[v]) and assertion vector (e.g. ‘[map?]) must be inside a third vector:
1
[[v] [map?]]
Example - Using CCC’s format to suck a map and spit a vector
The assertion vector can have any assertions supported by CCC. For example, here the constrained function below sucks a map and spits a vector:
1
2
3
4
5
6
7
;; Example - suck map and spit vector using CCC form
(def suck-map-spit-vector-fn1 (apply-contract-aspects (fn [m] (:c m)) [[v] [map? => vector?]]))
(will-work suck-map-spit-vector-fn1 {:a 1 :b 2 :c [1 2 3]})
(will-fail suck-map-spit-vector-fn1 {:a 1 :b 2 :c 1})
Example - Using CCC’s format to suck a map with keyword keys, and spit a vector
Or, additionally, to ensure the map’s keys are all keywords:
1
2
3
4
5
6
7
;; Example - suck map, spit vector but also all map keys are keywords
(def suck-map-keyword-keys-fn1 (apply-contract-aspects (fn [m] (:c m)) [[v] [map? (every? keyword? (keys v)) => vector?]]))
(will-work suck-map-keyword-keys-fn1 {:a 1 :b 2 :c [1 2 3]})
(will-fail suck-map-keyword-keys-fn1 {"x" 1 :b 2 :c 1})
Example - using CCC’s format with rich assertions
CCC supports the specification of rich assertions. For a two argument function (map, keyword), where the map’s keys are keywords, the values numbers; and the return value unconstrained, in CCC’s format, the full contract would look like this:
1
[[m k] [(map? m) (every? keyword (keys m)) (every? number? (vals m)) (keyword? k)]]
An example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;; Example - using CCC's format with rich assertions
;; In this example, the assertion constrains the function to suck a map,
;; with keywords keys and numeric values, and a keyword.
;; The returned value is unconstrained
(def map-keyword-keys-numeric-vals-fn2 (apply-contract-aspects (fn [m k] (k m)) [[m k] [(map? m) (every? keyword (keys m)) (every? number? (vals m)) (keyword? k)]]))
;; This will work and return nil as the return value is not constrained
(will-work map-keyword-keys-numeric-vals-fn2 {:a 1 :b 2 :c 3} :d)
(will-fail map-keyword-keys-numeric-vals-fn2 {:a 1 :b 2 :c 3} "d")
(will-fail map-keyword-keys-numeric-vals-fn2 {:a :x :b 2 :c 3} :a)
(will-fail map-keyword-keys-numeric-vals-fn2 {"x" 1 :b 2 :c 3} :d)
Example - using CCC’s format in a suck definition
You can also use a CCC form in a suck definition. Likely confusing, notably because you have to be quite careful as to what assertions are applied to which arguments, but it works. The CCC form works as if it is a mnemonic (see later) in the same position.
Note in the example below the map? assertion for the result in the CCC form has been discarded because it is not a suck assertion; the spit :number assertion is applied to the result.
1
2
3
4
5
6
7
8
9
10
;; Example - using CCC's format in a suck definition
;; Not the clearest way of specifying the contract
(def using-ccc-form-in-the-suck-definition-fn1 (apply-contract-aspects (fn [m k s] (k m)) {:suck [:map [[k s] [(keyword? k) (string? s) => map?]]] :spit :number} ))
(will-work using-ccc-form-in-the-suck-definition-fn1 {:a 1 :b 2 :c 3} :a "s2")
(will-fail using-ccc-form-in-the-suck-definition-fn1 {:a 1 :b 2 :c 3} "d" "s2")
(will-fail using-ccc-form-in-the-suck-definition-fn1 {:a :x :b 2 :c 3} :a 1 )
(will-fail using-ccc-form-in-the-suck-definition-fn1 {"x" 1 :b 2 :c 3} :d "s2")
Using Mnemonics
At their simplest, mnemonic are (Clojure) keyword “short-hands” for a contract assertion(s).
Using Mnemonics for Built-in Predicates
So far the assertions used have used Clojure’s built-in predicates such as map?, keyword? and vector? but we could have used their keyword mnemonics :map, :keyword or :vector. In fact any predicate of the form name? can be replaced by its keyword form :name (as long as the symbol can be resolved).
Example - using a built-in mnemonic
To repeat the example above using map? but with :map:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
;; Example - using a built-in mnemonic
;; This is a contrived example to show the symmetry when using a buit-in mnemonic.
;; BTW The function hard-codes a map as it return value so will always satisfy the spit constraint.
(def mnemonic-suck-and-spit-map-fn1 (apply-contract-aspects (fn [x] {:x 1 :y 2 :z 3}) :map))
;; This will work because the argument is a map and the (hard-coded) return value is a map
(will-work mnemonic-suck-and-spit-map-fn1 {:a 1 :b 2 :c 3})
;; But this fail sicne the argument is not a map
(will-fail mnemonic-suck-and-spit-map-fn1 [1 2 3])
Note: using a built-in mnemonic as the full contract definition will apply the assertion(s) to both the input argument and also return value.
Example - applying built-in mnemonics to individual arguments and the result
Repeating one of the examples above sucking a map and keyword and returning a vector, all that has changed is the assertions now use keywords.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;; Example - applying built-in mnemonics to individual arguments and the result
;; In this example, built-in mnemonics are used to constrains the
;; function to suck a map and keyword and spit a vector.
(def suck-map-keyword-spit-vector-fn1 (apply-contract-aspects (fn [m k] (k m)) {:suck [:map :keyword] :spit :vector}))
;; This will work as key :c contains a vector
(will-work suck-map-keyword-spit-vector-fn1 {:a 1 :b 2 :c [1 2 3]} :c)
;; But these will fail their contract
(will-fail suck-map-keyword-spit-vector-fn1 {:a 1 :b 2 :c 3} :c)
(will-fail suck-map-keyword-spit-vector-fn1 {:a 1 :b 2 :c 3} :d)
Note: built-in mnemonics in the map form of a contract definition apply the assertion only to the mnemonic’s corresponding argument.
Changing a Built-in Mnemonic Contract Definition
Replacing a built-in predicate with its keyword mnemonic is not a big win, just saving a few characters in the assertion definition.
The real power of mnemonics comes from the opportunity to change the definition of an existing mnemonic (or add custom ones - see later).
The configure-contracts-store macro manages mnemonics definitions.
Example - redefining the :map built-in mnemonic
Say you wanted to re-define the built-in :map mnemonic to check always that a map’s keys are keywords:
1
2
3
4
5
;; Changing a Built-in Mnemonic Contract Definition
;; Change the built-in :map mnemonics to also check the keys are keywords
(configure-contracts-store aspect-mnemonic-definitions {:map {:suck [[map? (every? keyword? (keys arg0))]]}})
Using the updated mnemonic is exactly the same as before:
1
2
3
4
5
6
7
8
9
10
11
12
13
;; Example - re-defining the :map built-in mnemonic
;; In this example, the :map built-in mnemonic has been changed to check the keys are keywords.
(def suck-map-keyword-spit-vector-fn1 (apply-contract-aspects (fn [m k] (k m)) {:suck [:map :keyword] :spit :vector}))
;; This will work as key :c contains a vector
(will-work suck-map-keyword-spit-vector-fn1 {:a 1 :b 2 :c [1 2 3]} :c)
;; But this will fail the contract as "x" is not a keyword.
(will-fail suck-map-keyword-spit-vector-fn1 {"x" 1 :b 2 :c 3} :c)
Adding and Using Custom Mnemonics
Just as you can update the definition of a built-in mnemonic, you can add / update your own custom mnemonics.
Example - using a custom mnemonic
Say you wanted to define a custom mnemonic that “packages” the assertions that a map’s keys are keywords and all the values are numeric:
1
2
3
4
5
6
7
8
;; Example - add a new mnemonic to the contracts store
;; The new mnemonic - :map-keyword-keys-numeric-vals - constrains an
;; argument to be a map with keyword keys and numeric values.
(configure-contracts-store
aspect-mnemonic-definitions
{:map-keyword-keys-numeric-vals {:suck [[map? (every? keyword? (keys arg0)) (every? number? (vals arg0))]]}})
To use the new mnemonic is straightforward. Note the mnemonic appears as the first value in the :suck assertion vector, the other entry being :keyword.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
;; Example - using a custom mnemonic
;; In this example, the assertion constrains the function to suck a map and keyword, spit a number.
;; The map must have keywords keys and numeric values.
(def mnemonic-suck-map-keyword-spit-number-fn1 (apply-contract-aspects (fn [m k] (k m)) {:suck [:map-keyword-keys-numeric-vals :keyword] :spit :number}))
;; This will work
(will-work mnemonic-suck-map-keyword-spit-number-fn1 {:a 1 :b 2 :c 3} :a)
;; But these will fail their contracts
(will-fail mnemonic-suck-map-keyword-spit-number-fn1 {:a :x :b 2 :c 3} :a)
(will-fail mnemonic-suck-map-keyword-spit-number-fn1 {:a 1 :b 2 :c 3} :d)
(will-fail mnemonic-suck-map-keyword-spit-number-fn1 {"x" 1 :b 2 :c 3} :c)
Using a Custom Mnemonic to package multiple arguments
You can go a step farther from the previous example and add the assertion for the second argument to be a keyword into the mnemonic as well:
1
2
3
4
5
6
7
;; Using a Custom Mnemonic to package multiple arguments
;; The new mnemonic combines the assertions to ensure the first argument
;; is a map with keyword keys and numerics value and also the requirement
;; for the second argument to be a keyword.
(configure-contracts-store aspect-mnemonic-definitions {:suck-map-keyword-keys-numeric-vals-and-keyword {:suck [[map? (every? keyword? (keys arg0)) (every? number? (vals arg0))] keyword?]}})
Example - using a custom multiple argument suck mnemonic
In this example a multiple argument mnemonic replaces the whole :suck definition.
1
2
3
4
5
6
7
8
9
10
11
12
;; Example - using a custom multiple argument suck mnemonic
;; In this example, the map assertion uses a mnemonic to ensure keywords keys and numeric values.
(def mnemonic-suck-map-keyword-spit-number-fn2 (apply-contract-aspects (fn [m k] (k m)) {:suck :suck-map-keyword-keys-numeric-vals-and-keyword :spit :number}))
;; Using the same tests as above
(will-work mnemonic-suck-map-keyword-spit-number-fn2 {:a 1 :b 2 :c 3} :a)
(will-fail mnemonic-suck-map-keyword-spit-number-fn2 {:a :x :b 2 :c 3} :a)
(will-fail mnemonic-suck-map-keyword-spit-number-fn2 {:a 1 :b 2 :c 3} :d)
(will-fail mnemonic-suck-map-keyword-spit-number-fn2 {"x" 1 :b 2 :c 3} :c)
Using a Custom Mnemonic to package the complete contract
Its just a small step from the multi argument example to packaging the whole contract in a custom mnemonic:
1
2
3
4
5
6
7
8
9
10
11
;; Using a Custom Mnemonic to package the complete contract
;; The custom mnemonic combines the assertions to ensure the first
;; argument is a map with keyword keys and numerics value and also the
;; requirement for the second argument to be a keywork. It also includes
;; the requirement for the return value to be a number.
(configure-contracts-store
aspect-mnemonic-definitions
{:contract-suck-map-keyword-keys-numeric-vals-and-keyword-spit-number
{:suck [[map? (every? keyword? (keys arg0)) (every? number? (vals arg0))] keyword?] :spit :number}})
Example - using a custom mnemonic to package the whole contract
In this example the complete contract mnemonic replaces the whole contract map form.
1
2
3
4
5
6
7
8
9
10
11
12
13
;; Example - using a custom mnemonic to package the whole contract
;; In this example, the a mnemonic packages the complete assertion
(def mnemonic-suck-map-keyword-spit-number-fn3
(apply-contract-aspects (fn [m k] (k m)) :contract-suck-map-keyword-keys-numeric-vals-and-keyword-spit-number))
;; Exactly the same tests as above
(will-work mnemonic-suck-map-keyword-spit-number-fn3 {:a 1 :b 2 :c 3} :a)
(will-fail mnemonic-suck-map-keyword-spit-number-fn3 {:a :x :b 2 :c 3} :a)
(will-fail mnemonic-suck-map-keyword-spit-number-fn3 {:a 1 :b 2 :c 3} :d)
(will-fail mnemonic-suck-map-keyword-spit-number-fn3 {"x" 1 :b 2 :c 3} :c)
Using Mnemonics in Custom Mnemonics
You can use mnemonics in the composition of other, richer mnemonics (although beware the infinite recursion gotcha mentioned below).
For example, create a custom mnemonic - :suck-map-special - to constrain a map to have keyword keys and numeric values, and use that mnemonic in another mnemonic - :suck-map-special-and-keyword - to include the keyword as the second argument. And finally use the second mnemonic to specify the full contract for a two argument function sucking the constrained map and a keyword, and also spitting a number - :contract-suck-map-special-and-keyword-spit-number.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
;; Using Mnemonics in Custom Mnemeonics
;; The first customer mnemonic constrains a map to have keyword keys and numeric values.
;; The second custome mnemonic speficiy the constrained map and a keyword as the second argument.
;; The third custom mnemonic uses the second mnemonic to build a
;; complete contract mnemonic for a two argument function sucking the
;; constrained map and a keyword, and spitting a number.
(configure-contracts-store
aspect-mnemonic-definitions
{:suck-map-special {:suck [[map? (every? keyword? (keys arg0)) (every? number? (vals arg0))]]}
:suck-map-special-and-keyword {:suck [:suck-map-special :keyword]}
:contract-suck-map-special-and-keyword-spit-number {:suck :suck-map-special-and-keyword :spit :number}})
Example - using a mnemonic containing mnemonics
The example is exactly the same as the one above, but the use of “sub” mnemonics is transparent.
1
2
3
4
5
6
7
8
9
10
11
12
;; Example - using a mnemonic containing mnemonics
;; In this example, the three level mnemonic packages the complete assertion
(def mnemonic-suck-map-special-keyword-spit-number-fn1 (apply-contract-aspects (fn [m k] (k m)) :contract-suck-map-special-and-keyword-spit-number))
;; Exactly the same tests as above
(will-work mnemonic-suck-map-special-keyword-spit-number-fn1 {:a 1 :b 2 :c 3} :a)
(will-fail mnemonic-suck-map-special-keyword-spit-number-fn1 {:a :x :b 2 :c 3} :a)
(will-fail mnemonic-suck-map-special-keyword-spit-number-fn1 {:a 1 :b 2 :c 3} :d)
(will-fail mnemonic-suck-map-special-keyword-spit-number-fn1 {"x" 1 :b 2 :c 3} :c)
Composing Mnemonics - resolving arguments
In the examples above, mnemonics were always the first entry in the value of a suck or spit key - see the three level composed mnemonic immediately above.
Most the time the assertion (e.g. :map) did not need to include (specify) the name (symbol) of the argument the assertion would be applied to; the name was deduced from the assertion’s position in the value of the suck / spit key.
The only time an explicit argument name appeared was arg0 in the every? assertion clauses because the map was the first argument.
But what if the map was not the first argument?
Lets recast the :suck-map-special-and-keyword mnemonic to expect the :keyword first and the :map-special second but continue to use the :suck-map-special mnemonic even though the latter expects (and defines) the map to be arg0:
1
2
3
4
(configure-contracts-store
aspect-mnemonic-definitions
{:suck-keyword-and-map-special {:suck [:keyword :suck-map-special]}
:contract-suck-keyword-and-map-special-spit-number {:suck :suck-keyword-and-map-special :spit :number}})
Example - swapping the keyword and map in the three level composed mnemonics
An example using the swapped argument third level mnemonic :contract-suck-keyword-and-map-special-spit-number
1
2
3
4
5
6
7
8
9
10
11
12
;; Example - swapping the keyword and map in the three level composed mnemonics
;; In this example, the keyword and map are swapped in the three level mnemonic
(def mnemonic-suck-keyword-map-special-spit-number-fn1 (apply-contract-aspects (fn [k m] (k m)) :contract-suck-keyword-and-map-special-spit-number ))
;; The same tests as above but the arguments swapped
(will-work mnemonic-suck-keyword-map-special-spit-number-fn1 :a {:a 1 :b 2 :c 3})
(will-fail mnemonic-suck-keyword-map-special-spit-number-fn1 :a {:a :x :b 2 :c 3})
(will-fail mnemonic-suck-keyword-map-special-spit-number-fn1 :d {:a 1 :b 2 :c 3})
(will-fail mnemonic-suck-keyword-map-special-spit-number-fn1 :c {"x" 1 :b 2 :c 3})
The behaviour is as expected but the generated contract look similar to this:
1
2
;; Example - swapping the keyword and map in the three level composed mnemonics
(clojure.core.contracts/contract ctx-aspect2879 "\"ctx-aspect2879\"" [arg0 arg1] [(keyword? arg0) (map? arg1) (every? keyword? (keys arg1)) (every? number? (vals arg1)) => (number? %)])
Some notes:
-
The arg0 in the canonical definition of the :map-special mnemonic has been automatically rewritten in the final contract to be arg1 i.e. the second argument. argo refers to the :keyword (first) argument.
-
More generally, explicitly specified arguments in a mnemonic are automatically shifted right to whatever position the mnemonic has in the assertion clause. This applies recursively for composed mnemonics.
-
So when creating mnemonics, if you need to use explicit argument names (arg0, arg1, arg2, etc), name them relative to the mnemonic’s argument order and they can be composed successfully.
Example - using absolute arguments in mnemonics
Much (most?) of the time relative arguments names suffice. But there may be times when using composed mnemonics when you need to specify (refer to) absolute argument names.
A rather contrived scenario: say you needed to define mnemonics with relative arguments but use an absolute argument inside the relative mnemonic. Concretely: e.g. if the first argument is a map but the third (relative) argument must be a keyword that is a key in the map.
Note the :keyword-in-first-argument-map below uses arg0 to refer to itself (i.e. the keyword) but abs-arg0 to refer to the first argument (i.e. the map).
1
2
3
(configure-contracts-store
aspect-mnemonic-definitions
{:keyword-in-first-argument-map {:suck [[:keyword (contains? abs-arg0 arg0)]]}})
The example follows the familiar format:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;; Example - using absolute arguments in mnemonics
;; This function takes a map, string and keyword, and returns a number.
;; The map must have keyword keys and numberic values.
;; The keyword must exist in the map
(def absolute-argument-mnemonic-fn1 (apply-contract-aspects (fn [m s k] (k m)) {:suck [:suck-map-special :string :keyword-in-first-argument-map] :spit :number}))
;; The same tests as above but the arguments swapped
(will-work absolute-argument-mnemonic-fn1 {:a 1 :b 2 :c 3} "s1" :a)
(will-fail absolute-argument-mnemonic-fn1 {:a :x :b 2 :c 3} "s1" :a)
(will-fail absolute-argument-mnemonic-fn1 {:a 1 :b 2 :c 3} "s1" :d)
(will-fail absolute-argument-mnemonic-fn1 {"x" 1 :b 2 :c 3} "s1" :c)
For reference, the contract looks like the below, the abs-arg0 has been rewritten to arg0 while the arg0 in the :keyword-in-first-argument-map mnemonic has been rewritten to arg2.
1
(clojure.core.contracts/contract ctx-aspect2879 "\"ctx-aspect2879\"" [arg0 arg1 arg2] [(map? arg0) (every? keyword? (keys arg0)) (every? number? (vals arg0)) (string? arg1) (keyword? arg2) (contains? arg0 arg2) => (number? %)])
Beware mnemonic gotchas
The code tries to be as aggressive as possible to catch inconsistencies and ensure your get what you want. But there are some things to be aware of.
Beware mnemonic gotchas - infinite recursion
Because mnemonics can use other mnemonic in their definition there is the ability to create an infinite loop if a “downstream” mnemonic refers to an “upstream” one.
Its possible to “remember” used mnemonics during evaluation but not done so yet - on the list of improvments.
Beware mnemonic gotchas - incompatible argument assertions
If a custom mnemonic’s argument assertions conflict with an explicit predicate, built-in mnemonic (e.g. :map) or another custom mnemonic, the contract will include more than one, but potentially incompatible, assertions for the same argument. Which may fail miserably.
Note though that duplicate assertions for the same argument will be distinct-ified and cause no issue.
Beware mnemonic gotchas - unexpected arguments
If a custom mnemonic with two arguments is applied to a function expecting e.g. only one argument, an error will occur at run time.
Contracts with Multiple Arities
CCC supports contracts for functions with multiple arities.
CHUGAR supports multiple arities, just put them all in a vector on the call to e.g. apply-contract-aspects.
CHUGAR raises an error if it identifies contracts with the same arity for the same function in the same call to the macro (e.g. apply-contract-aspects).
Example - two arities (map => number) and (map,keyword => vector)
This example of a multiple arities contract defines one arity for a single argument function that suck a map and returns a vector; and a second arity for a two argument function that sucks a map and keyword and spits a vector.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
;; Example - two arities (map => number) and (map,keyword => number)
;; This is the target function with two arities
(defn two-arity-fn1
([m] (:a m))
([m k] (k m)))
;; The constrained function
(def constrained-two-arity-fn1 (apply-contract-aspects two-arity-fn1 [{:suck :map :spit :number} {:suck [:map :keyword] :spit :vector}]))
;; First Arity Tests
;; This will works as value of key :a is a number
(will-work constrained-two-arity-fn1 {:a 1 :b 2 :c [1 2 3]})
; This will fail as value of key :a is not a number
(will-fail constrained-two-arity-fn1 {:a "x"})
;; Second Arity Tests
;; This will work as value of key :c is a vector
(will-work constrained-two-arity-fn1 {:a 1 :b 2 :c [1 2 3]} :c)
; This will fail as value of key :d is not a vector (its nil)
(will-fail constrained-two-arity-fn1 {:a "x"} :d)
Example - multiple arities using mixed CCC form and map form
The definition of the contract for each arity can be either CCC form or map form; they can be mixed as well.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
;; Example - multiple arities using mixed CCC form and map form
;; The same multiple arity example as above but using a mixed contract definition with CCC form and map form.
(def constrained-two-arity-fn1 (apply-contract-aspects two-arity-fn1 [[[m] [map? => number?]] {:suck [:map :keyword] :spit :vector}]))
;; First Arity Tests
;; This will works as value of key :a is a number
(will-work constrained-two-arity-fn1 {:a 1 :b 2 :c [1 2 3]})
; This will fail as value of key :a is not a number
(will-fail constrained-two-arity-fn1 {:a "x"})
;; Second Arity Tests
;; This will work as value of key :c is a vector
(will-work constrained-two-arity-fn1 {:a 1 :b 2 :c [1 2 3]} :c)
; This will fail as value of key :d is not a vector (its nil)
(will-fail constrained-two-arity-fn1 {:a "x"} :d)
Final Words
CHUGAR’s genesis was as part of a larger project (other parts to be published soon).
Writing the project has taught me a lot about Clojure (notably macros and protocols) and its ecosystem (testing, profiling, Clojars and suchlike) but I still have lots to learn.
I’m sure more experienced Clojurians would have some head-scratching moments if they looked at the code. As I tweeted recently, I think the biggest challenge to learning a new language is to design idiomatically and well in it. All advice on that subject gratefully received and acknowledged.
The whole point of CHUGAR was/is to make using the rich features of CCC as easy as possible. I hope it (begins to) succeed on that criterion and believe mnemonics offers an original contribution and productivity aid for defining, re-using and composing contract aspects.
I already have another article in the works (part of the same project) on the practical and concrete use of CHUGAR to apply aspect contracts to the values of map keys. Coming soon!
Final Final Words
The overall project is the first “serious” (as opposed to dabbling) Clojure code I’ve written. And the first serious code in a functional language.
In all my time writing software, I can’t ever remember learning a new language that just gets out of the way when I’m rattling along, but gets in the way when I’m stuck and need some help overcoming an implementation or design issue, offering a (new to me) feature to use, or an approach to apply, to elide the obstacle. Clojure, as many acknowledge, rocks!