Social Contracts
(require contract/social) | package: social-contract |
High-level, composable syntax for describing contracts.
1 Introduction
Racket’s built-in contract DSL is a flexible way to describe arbitrary data types. An essential property of data is that it exhibits hierarchical structure. Yet, the contract DSL does not provide corresponding syntactic abstractions with which to express this structure, necessitating that contract authors spell out the details of each contract in terms of primitives such as -> and or/c. In this sense, it is a low-level language for describing the contract, analogous to describing the operation of addition in terms of combinational logic.
The present module provides the missing abstractions, giving us a high level DSL for describing contracts in terms of their phrase structure. The forms provided correspond to commonly encountered high-level data abstractions whose definitions are collectively agreed upon. This agreement may take the form of collaborative development and discussion on the source repository, to agree on, for instance, the most useful form and variations of a predicate contract, or, at least, the agreement may simply be tacit in the sense that these contracts correspond to ideas that are common and widely known.
As an example, when you take two values of the same type and produce another value of the same type, this is an instance of composition. With the built-in contract DSL, the contract for this function may be specified as something like (-> integer? integer? integer?), which does not encode the idea of composition. Instead, here, we simply use the appropriate high-level contract, for instance (binary-composition/c integer?).
As the forms also compose, complex contracts exhibiting phrase structure have compact representations. For instance, (-> (-> integer? integer?) (-> integer? integer?) (-> integer? integer?)) may be expressed as (binary-composition/c (self-map/c integer?)).
2 What if I Don’t See the Contract I Need?
If you believe that the data you are describing could be expressed more economically than the forms allow, or if an appropriate contract is missing altogether and seems relatively general or common (e.g. you’ve needed this contract more than once, and suspect that others might, as well), consider bringing it up for possible addition. With enough support or motivation, it will be added.
3 How Do I Migrate My Existing Contracts?
In addition to the forms in contract/social, this collection also includes a racket-contract-to-social-contract "reverse compiler," which can help you migrate any existing contracts you’ve already written. You can use this either interactively, passing it one contract at a time to help you migrate your contracts manually, or you could also give it an entire provide form at once, to translate them all wholesale. See "C3PO": Social Protocol Assistant for more on how to use this. Note that although you might prefer to, it isn’t strictly necessary to migrate existing contracts, since the forms in this module can be used alongside them and even compose with them.
4 Contracts
4.1 Basic and Arity-Based Forms
syntax
syntax
(function/c source/c ... target/c)
If used as an identifier macro, function/c means a unary function with input and output of type any/c.
function/c in general is equivalent to ->.
> (define/contract (list-length lst) (function/c list? natural-number/c) (if (empty? lst) 0 (add1 (list-length (rest lst))))) > (list-length '(h e l l o)) 5
> (list-length "hello") list-length: contract violation
expected: list?
given: "hello"
in: the 1st argument of
(-> (listof any/c) natural-number/c)
contract from: (function list-length)
blaming: program
(assuming the contract is correct)
at: eval:1:0
thunk/c in general is equivalent to (-> target/c).
> (define/contract (hello-button) thunk/c "hello!") > (hello-button) "hello!"
> (hello-button "friend") hello-button: arity mismatch;
the expected number of arguments does not match the given
number
expected: 0
given: 1
arguments...:
"friend"
> (define/contract (hello-button) (thunk/c number?) "hello!") > (hello-button) hello-button: broke its own contract
promised: number?
produced: "hello!"
in: the range of
(-> number?)
contract from: (function hello-button)
blaming: (function hello-button)
(assuming the contract is correct)
at: eval:7:0
syntax
syntax
(binary-function/c a/c b/c target/c)
binary-function/c in general is equivalent to (-> a/c b/c target/c).
Where applicable, prefer a more specific contract like binary-predicate/c, binary-operation/c, binary-composition/c, or binary-constructor/c.
> (define/contract (my-add-as-string a b) (binary-function/c number? number? string?) (number->string (+ a b))) > (my-add-as-string 3 5) "8"
> (my-add-as-string 5) my-add-as-string: arity mismatch;
the expected number of arguments does not match the given
number
expected: 2
given: 1
arguments...:
5
syntax
syntax
(variadic-function/c source/c)
syntax
(variadic-function/c source/c target/c)
syntax
(variadic-function/c paramspec)
syntax
(variadic-function/c source/c paramspec)
syntax
(variadic-function/c source/c target/c paramspec)
paramspec = (head arg/c ...) | (tail arg/c ...)
variadic-function/c in general is equivalent to (-> source/c ... target/c).
Where applicable, prefer a more specific contract like variadic-predicate/c, variadic-composition/c, or variadic-constructor/c.
> (define/contract (my-add-as-string . vs) (variadic-function/c number? string?) (number->string (apply + vs))) > (my-add-as-string 3 5 7) "15"
4.2 Transformations
syntax
(self-map/c type/c)
syntax
(self-map/c type/c paramspec)
paramspec = (head arg/c ...) | (tail arg/c ...)
self-map/c in general is equivalent to (-> type/c type/c), and for instance, (self-map/c list? (head number?)) is equivalent to (-> number? list? list?).
> (define/contract (double n) (self-map/c natural-number/c) (* n 2)) > (double 5) 10
> (double "hello") double: contract violation
expected: natural-number/c
given: "hello"
in: the 1st argument of
(-> natural-number/c natural-number/c)
contract from: (function double)
blaming: program
(assuming the contract is correct)
at: eval:14:0
> (define/contract (prefix str n) (self-map/c string? (tail natural-number/c)) (substring str 0 n)) > (prefix "apple" 3) "app"
> (prefix (list 1 2 3 4 5) 3) prefix: contract violation
expected: string?
given: '(1 2 3 4 5)
in: the 1st argument of
(-> string? natural-number/c string?)
contract from: (function prefix)
blaming: program
(assuming the contract is correct)
at: eval:17:0
syntax
Equivalent to (self-map/c procedure?) or (-> procedure? procedure?).
syntax
(binary-constructor/c primitive/c composite/c)
syntax
(binary-constructor/c #:order order primitive/c composite/c)
syntax
(variadic-constructor/c primitive/c composite/c)
syntax
(variadic-constructor/c #:order order primitive/c composite/c)
This contract is equivalent to self-map/c when the latter expects a single additional argument, and in such cases the appropriate contract should be selected based on whether the function actually entails a notion of "construction" or not.
binary-constructor/c is equivalent to (-> primitive/c composite/c composite/c) or (-> composite/c primitive/c composite/c), depending on the indicated order, and variadic-constructor/c is equivalent to (-> primitive/c ... composite/c composite/c) or (-> composite/c primitive/c ... composite/c).
> (define/contract (my-cons elem lst) (binary-constructor/c any/c list?) (cons elem lst)) > (my-cons "apple" (list "banana" "cherry")) '("apple" "banana" "cherry")
> (my-cons "apple" "banana") my-cons: contract violation
expected: list?
given: "banana"
in: the 2nd argument of
(-> any/c (listof any/c) (listof any/c))
contract from: (function my-cons)
blaming: program
(assuming the contract is correct)
at: eval:20:0
4.3 Operations
syntax
(operation/c n source/c target/c)
syntax
(operation/c n source/c)
syntax
(operation/c n source/c target/c paramspec)
syntax
(operation/c n source/c paramspec)
syntax
(binary-operation/c source/c target/c)
syntax
(binary-operation/c source/c)
syntax
(binary-operation/c source/c target/c paramspec)
syntax
(binary-operation/c source/c paramspec)
paramspec = (head arg/c ...) | (tail arg/c ...)
binary-operation/c recognizes an operation of arity 2. Note that the term "binary operation" isn’t used consistently in mathematical literature, where it sometimes refers specifically to a closed binary operation where the output is of the same type as the input. For this more specific case, use binary-composition/c instead.
As an example, (operation/c 3 source/c target/c) is equivalent to (-> source/c source/c source/c target/c), while (binary-operation/c source/c target/c) is equivalent to (-> source/c source/c target/c).
> (define/contract (my-add-as-string a b) (binary-operation/c number? string?) (number->string (+ a b))) > (my-add-as-string 3 5) "8"
> (my-add-as-string 5) my-add-as-string: arity mismatch;
the expected number of arguments does not match the given
number
expected: 2
given: 1
arguments...:
5
syntax
(composition/c n type/c)
syntax
(composition/c n type/c paramspec)
syntax
(binary-composition/c type/c)
syntax
(binary-composition/c n type/c paramspec)
syntax
(variadic-composition/c type/c)
syntax
(variadic-composition/c type/c paramspec)
paramspec = (head arg/c ...) | (tail arg/c ...)
binary-composition/c is equivalent to (-> type/c type/c type/c); variadic-composition/c is equivalent to (-> type/c ... type/c); and for instance, (composition/c 3 type/c) is equivalent to (-> type/c type/c type/c type/c).
4.4 Encoding
syntax
syntax
(predicate/c source/c)
syntax
syntax
(binary-predicate/c source/c)
syntax
(binary-predicate/c a/c b/c)
syntax
syntax
(variadic-predicate/c source/c)
syntax
(variadic-predicate/c source/c paramspec)
paramspec = (head arg/c ...) | (tail arg/c ...)
predicate/c is equivalent to (-> source/c boolean?), binary-predicate/c is equivalent to (-> a/c b/c boolean?), and variadic-predicate/c is equivalent to (-> source/c ... boolean?), with source/c, a/c and b/c defaulting to any/c if left unspecified.
encoder/c is equivalent to (-> any/c as-type/c), decoder/c is equivalent to (-> from-type/c any/c), and hash-function/c is equivalent to (-> any/c fixnum?).
4.5 Data
Equivalent to (or/c type/c default/c), with default/c defaulting to #f if left unspecified.
> (define/contract v (maybe/c number?) 5) > (define/contract v (maybe/c number?) #f) > (define/contract v (maybe/c number? void?) (void)) > (define/contract v (maybe/c number?) "5") v: broke its own contract
promised: (or/c number? #f)
produced: "5"
in: (or/c number? #f)
contract from: (definition v)
blaming: (definition v)
(assuming the contract is correct)
at: eval:29:0
syntax
(nonempty/c type/c)
> (define/contract v (nonempty/c list?) (list 1 2 3)) > (define/contract v (nonempty/c string?) "hello") > (define/contract v (nonempty/c sequence?) "hello") > (define/contract v (nonempty/c string?) "") v: broke its own contract
promised: (not/c empty?)
produced: ""
in: an and/c case of
(and/c string? (not/c empty?))
contract from: (definition v)
blaming: (definition v)
(assuming the contract is correct)
at: eval:33:0
> (define/contract v (nonempty/c list?) (list)) v: broke its own contract
promised: (not/c empty?)
produced: '()
in: an and/c case of
(and/c (listof any/c) (not/c empty?))
contract from: (definition v)
blaming: (definition v)
(assuming the contract is correct)
at: eval:34:0
4.6 Sequences
syntax
syntax
(lift/c pure/c)
syntax
(lift/c pure/c functor/c)
syntax
(lift/c paramspec)
syntax
(lift/c pure/c paramspec)
syntax
(lift/c pure/c functor/c paramspec)
paramspec = (head arg/c ...) | (tail arg/c ...)
Equivalent in general to (-> pure/c (functor/c pure/c)).
> (define/contract (my-range n) (lift/c number? listof) (range n)) > (my-range 5) '(0 1 2 3 4)
> (my-range "5") my-range: contract violation
expected: number?
given: "5"
in: the 1st argument of
(-> number? (listof number?))
contract from: (function my-range)
blaming: program
(assuming the contract is correct)
at: eval:35:0
syntax
syntax
(map/c source/c)
syntax
(map/c source/c paramspec)
syntax
(map/c source/c target/c)
syntax
(map/c source/c target/c paramspec)
paramspec = (head arg/c ...) | (tail arg/c ...)
(map/c any/c sequence?) is equivalent to (lift/c sequence? sequenceof) but they indicate different things. The former should be used in cases where the sequences in the output are each derived from elements of the input (e.g. a function mapping nodes in a graph to a list of their neighbors), while the latter should be used where the sequences in the input are derived from the input sequence as a whole (e.g. permutations of the input sequence).
Equivalent in general to (-> (sequenceof source/c) (sequenceof target/c)).
> (define/contract (rotate n lst) (map/c number? (head number?)) (append (drop lst n) (take lst n))) > (rotate 3 (list 1 2 3 4 5)) '(4 5 1 2 3)
> (rotate 3 (list "a" "p" "p" "l" "e")) rotate: contract violation
expected: number?
given: "a"
in: an element of
the 2nd argument of
(->
number?
(sequenceof number?)
(sequenceof number?))
contract from: (function rotate)
blaming: program
(assuming the contract is correct)
at: eval:38:0
Changed in version 2.0 of package social-contract: The former map/c was renamed to mapper/c and this more versatile version was introduced as map/c.
Equivalent to (map/c source/c target/c (head (function/c source/c target/c))) or (-> (-> source/c target/c) (sequenceof source/c) (sequenceof target/c)).
> (define/contract (stringify-numbers fn lst) (mapper/c number? string?) (map fn lst)) > (stringify-numbers number->string (list 1 2 3 4)) '("1" "2" "3" "4")
> (stringify-numbers number->string (list "1" "2" "3" "4")) stringify-numbers: contract violation
expected: number?
given: "1"
in: an element of
the 2nd argument of
(->
(-> number? string?)
(sequenceof number?)
(sequenceof string?))
contract from: (function stringify-numbers)
blaming: program
(assuming the contract is correct)
at: eval:41:0
Equivalent to (map/c type/c (head (predicate/c type/c))) or (-> (-> type/c boolean?) (sequenceof type/c) (sequenceof type/c)).
> (define/contract (take-while pred lst) (filter/c number?) (if (or (null? lst) (not (pred (car lst)))) null (cons (car lst) (take-while pred (cdr lst))))) > (take-while positive? (list 4 2 7 -1 2 3 -4 5)) '(4 2 7)
> (take-while positive? (list "1" "2" "3" "4")) take-while: contract violation
expected: number?
given: "1"
in: an element of
the 2nd argument of
(->
(-> number? boolean?)
(sequenceof number?)
(sequenceof number?))
contract from: (function take-while)
blaming: program
(assuming the contract is correct)
at: eval:44:0
syntax
syntax
(reducer/c type/c)
syntax
(reducer/c type/c target/c)
syntax
(reducer/c paramspec)
syntax
(reducer/c type/c paramspec)
syntax
(reducer/c type/c target/c paramspec)
paramspec = (head arg/c ...) | (tail arg/c ...)
If target/c is not specified, it is assumed to be type/c. If type/c isn’t specified either, they are both assumed to be any/c.
reducer/c is in general equivalent to (function/c (sequenceof type/c) target/c) or (-> (sequenceof type/c) target/c).
> (define/contract (my-sum lst) (reducer/c number?) (apply + lst)) > (my-sum (list 1 2 3 4)) 10
> (my-sum (list "1" "2" "3" "4")) my-sum: contract violation
expected: number?
given: "1"
in: an element of
the 1st argument of
(-> (sequenceof number?) number?)
contract from: (function my-sum)
blaming: program
(assuming the contract is correct)
at: eval:47:0
syntax
syntax
(classifier/c by-type/c)
Equivalent to (map/c any/c sequence? (head (encoder/c by-type/c))) or (-> (-> any/c by-type/c) sequence? (sequenceof sequence?)).
> (define/contract (alphabetize key lst) (classifier/c char?) (group-by key lst)) > (alphabetize (curryr string-ref 0) (list "apple" "banana" "apricot" "cherry" "blackberry")) '(("apple" "apricot") ("banana" "blackberry") ("cherry"))
> (alphabetize string-upcase (list "apple" "banana" "apricot" "cherry" "blackberry")) alphabetize: contract violation
expected: char?
given: "APPLE"
in: the range of
the 1st argument of
(->
(-> any/c char?)
(sequenceof any/c)
(sequenceof sequence?))
contract from: (function alphabetize)
blaming: program
(assuming the contract is correct)
at: eval:50:0
5 "C3PO": Social Protocol Assistant
(require contract/social/c3po) | package: social-contract |
C3PO is a "reverse compiler" that can help you migrate your contracts to social contracts. It accepts contracts, either individually or as an entire provide form, and translates the input so it is rephrased in terms of high-level social contracts. It can even accept social contracts you’ve already written and translate them into more minimal representations, so it could potentially be incorporated into a general-purpose linter for contracts.
To use it, simply translate a contract or provide specification (provided verbatim without quoting) in order to "reverse-compile" it into high-level social contracts.
syntax
(translate ctc)
(translate (-> integer? integer? integer?)) (translate (-> any/c number?)) (translate (-> string? any/c)) (translate (-> (-> integer? integer? integer?) (-> integer? integer? integer?))) (translate (provide address occupation (contract-out [name (-> any/c string?)])))
5.1 Limitations
You should use C3PO as an assistant and not defer to it blindly, due to the following limitations.
5.1.1 Correct Vs Appropriate Contracts
Low-level contract specifications cannot in general be uniquely mapped to high level contract representations, and in some cases a matching high-level contract may coincidentally have the same signature but not actually describe the data in question. For instance, a function that takes a number and a list and returns a list has the signature of a constructing function, yet, this particular function may be using the input number as an index of some kind to extract a sublist rather than incorporating it into the resulting list as a constructor typically would. We may prefer to think of this as a parametrized self-map or as a binary function rather than as a binary constructor. It all boils down to the question, "what is the idea behind this function?" – which can’t always be discerned from the function signature alone.
5.1.2 Not All Contract Forms Supported
At the moment, C3PO supports the -> and ->* contract forms, but not ->i. If ->i is encountered during parsing, it would just leave this form unchanged in the output (while translating the rest of it). Note that this is a limitation of C3PO, not the forms in contract/social, which are fully compatible with the built-in forms.