11 Enumerations
(require data/enumerate) | package: data-enumerate-lib |
This library defines enumerations. Enumerations are represented as bijections between the natural numbers (or a prefix of them) and the values of some contract. Most of the enumerations defined in this library guarantee that the constructed bijection is efficient, in the sense that decoding a number is roughly linear in the number of bits taken to represent the number.
> (from-nat (list/e natural/e natural/e natural/e) 0) '(0 0 0)
> (from-nat (list/e natural/e natural/e natural/e) 1) '(0 0 1)
> (from-nat (list/e natural/e natural/e natural/e) (expt 2 100)) '(10822639409 2238661967 110761420)
> (to-nat (list/e natural/e natural/e natural/e) (list 123456789 123456789 123456789)) 1881676417513891481838999
> (from-nat (or/e natural/e (list/e natural/e natural/e)) 0) 0
> (from-nat (or/e natural/e (list/e natural/e natural/e)) 1) '(0 0)
> (from-nat (or/e natural/e (list/e natural/e natural/e)) 2) 1
> (from-nat (or/e natural/e (list/e natural/e natural/e)) 3) '(0 1)
> (from-nat (or/e natural/e (list/e natural/e natural/e)) 4) 2
(define bt/e (delay/e (or/e (single/e #f) (list/e bt/e bt/e))))
> (from-nat bt/e 0) #f
> (from-nat bt/e 1) '(#f #f)
> (from-nat bt/e 2) '(#f (#f #f))
> (from-nat bt/e 3) '((#f #f) #f)
> (from-nat bt/e (expt 2 100))
'(((((((#f #f) #f) (#f (#f #f))) (((#f (#f #f)) (#f #f)) ((#f #f) #f)))
(((#f #f) (#f #f)) (#f ((#f #f) (#f (#f #f))))))
(((((#f #f) #f) (#f (#f #f))) (((#f (#f #f)) (#f #f)) ((#f #f) #f)))
(((#f #f) #f) (#f ((#f #f) (#f (#f #f)))))))
((((((#f #f) #f) (#f (#f #f))) (((#f (#f #f)) (#f #f)) ((#f #f) #f)))
(((#f #f) (#f #f)) (#f ((#f #f) (#f (#f #f))))))
(((((#f #f) #f) (#f (#f #f))) (((#f (#f #f)) (#f #f)) ((#f #f) #f)))
(((#f #f) #f) (#f ((#f #f) (#f (#f #f))))))))
(define ordered-pair/e (cons/de [hd natural/e] [tl (hd) (nat+/e (+ hd 1))]))
> (for/list ([i (in-range 10)]) (from-nat ordered-pair/e i))
'((0 . 1)
(0 . 2)
(1 . 2)
(1 . 3)
(0 . 3)
(1 . 4)
(2 . 3)
(2 . 4)
(2 . 5)
(0 . 4))
(define (swap-components x) (cons (cdr x) (car x))) (define other-ordered-pair/e (map/e swap-components swap-components ordered-pair/e #:contract (cons/c exact-nonnegative-integer? exact-nonnegative-integer?)))
> (for/list ([i (in-range 10)]) (from-nat ordered-pair/e i))
'((0 . 1)
(0 . 2)
(1 . 2)
(1 . 3)
(0 . 3)
(1 . 4)
(2 . 3)
(2 . 4)
(2 . 5)
(0 . 4))
> (map/e (λ (x) (floor (/ x 100))) (λ (x) (* x 100)) natural/e #:contract exact-nonnegative-integer?) map/e: contract violation;
new enumeration would not be two-way
passing 157 to `from-nat` produces:
157
which, when passed through `in' and `out', produces:
100
which, when passed to `to-nat' produces 100,
but it should have been 157
in: (->i
((in
(e es c)
(dynamic->*
#:mandatory-domain-contracts
(map enum-contract (cons e es))
#:range-contracts
(list c)))
(out
(e es c)
(dynamic->*
#:mandatory-domain-contracts
(list c)
#:range-contracts
(map enum-contract (cons e es))))
(e enum?)
#:contract
(c contract?))
#:rest
(es (listof enum?))
#:pre/desc
(in out e es)
(appears-to-be-a-bijection?
in
out
(cons e es))
(result enum?))
contract from:
<pkgs>/data-enumerate-lib/data/enumerate.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/data-enumerate-lib/data/enumerate.rkt:45.3
Sometimes, there is no easy way to make two functions that form a bijection. In that case you can use pam/e and supply only one function to make a one way enumeration. For example, we can make an enumeration of picts of binary trees like this:
(define pict-bt/e (pam/e (λ (bt) (binary-tidier (let loop ([bt bt]) (cond [(list? bt) (apply tree-layout (map loop bt))] [else #f])))) bt/e #:contract pict?))
> (from-nat pict-bt/e 10) > (from-nat pict-bt/e 11) > (from-nat pict-bt/e 12)
Putting all these pieces together, here is a definition of an enumeration of closed expressions of the untyped lambda calculus.
(define/contract (lc-var/e bvs memo) (-> (set/c symbol?) (hash/c (set/c symbol?) enum?) enum?) ; memoization is a significant performance improvement (hash-ref! memo bvs (delay/e (or/e ; the variables currently in scope (apply fin/e (set->list bvs)) ; the λ case; first we build a dependent ; pair of a bound variable and a body expression ; and then use map/e to build the usual syntax (map/e (λ (pr) `(λ (,(car pr)) ,(cdr pr))) (λ (λ-exp) (cons (caadr λ-exp) (caddr λ-exp))) (cons/de [hd symbol/e] [tl (hd) (lc-var/e (set-add bvs hd) memo)]) #:contract (list/c 'λ (list/c symbol?) lc-exp?)) ; application expressions (list/e (lc-var/e bvs memo) (lc-var/e bvs memo)))))) (define (lc-exp? x) (match x [(? symbol?) #t] [`(λ (,x) ,e) (and (symbol? x) (lc-exp? e))] [`(,a ,b) (and (lc-exp? a) (lc-exp? b))])) (define lc/e (lc-var/e (set) (make-hash)))
> (from-nat lc/e 0) '(λ (a) a)
> (from-nat lc/e 1) '((λ (a) a) (λ (a) a))
> (from-nat lc/e 2) '(λ (a) (λ (a) a))
> (to-nat lc/e '(λ (f) ((λ (x) (f (x x))) (λ (x) (f (x x)))))) 65533604995627347825568080010
11.1 Enumeration Properties
In addition to the functions that form the bijection, an enumeration also has a contract, a count, and three boolean properties associated with it: if it is finite or not, if it is a bijection to the natural numbers or merely maps from the natural numbers without going back, and if the contract it has is a flat-contract?.
The functions in this section are predicates for the boolean properties and selection functions for other properties.
When an enumeration prints out, it shows the first few elements of the enumeration and, if it is either a finite enumeration or a one way enumeration, it prints finite and one-way, as appropriate. If those prefixes are not printed, then the enumeration is not finite and is not one-way.
procedure
(finite-enum? v) → boolean?
v : any/c
procedure
(infinite-enum? v) → boolean?
v : any/c
procedure
(two-way-enum? v) → boolean?
v : any/c
procedure
(one-way-enum? v) → boolean?
v : any/c
procedure
(flat-enum? v) → boolean?
v : any/c
procedure
e : finite-enum?
procedure
e : finite-enum?
11.2 Querying Enumerations
The functions in this section exercise the enumeration, turning natural numbers back and forth to the values that an enumeration enumerates.
procedure
(from-nat e n) → (enum-contract e)
e : enum?
n :
(if (finite-enum? e) (integer-in 0 (enum-count e)) exact-nonnegative-integer?)
procedure
(to-nat e x) →
(if (finite-enum? e) (integer-in 0 (enum-count e)) exact-nonnegative-integer?) e : two-way-enum? x : (enum-contract e)
procedure
(enum->list e [n]) → (listof (enum-contract e))
e : enum?
n :
(if (finite-enum? e) (integer-in 0 (enum-count e)) exact-nonnegative-integer?) = (enum-count e)
If n is not supplied, then e must be a finite-enum.
Examples: | ||||
|
Note that enumerations are also sequences directly, too.
Example: | ||||
|
11.3 Constructing Enumerations
This section contains the fundamental operations for building enumerations.
value
Examples: | ||||
|
procedure
(below/e max) →
(and/c (if (= max +inf.0) finite-enum? infinite-enum?) two-way-enum? flat-enum?) max : (or/c exact-nonnegative-integer? +inf.0)
Example: | ||
|
value
Example: | ||
|
procedure
f : (-> (enum-contract e) c) f-inv : (-> c (enum-contract e)) c : contract? e : enum? (map/e f f-inv #:contract c e ...+) → enum?
f :
(dynamic->* #:mandatory-domain-contracts (map enum-contract e) #:range-contracts (list c))
f-inv :
(dynamic->* #:mandatory-domain-contracts (list c) #:range-contracts (map enum-contract e)) c : contract? e : enum?
If multiple enumerations are supplied, f is expected to accept any combination of elements of the given enumerations, i. e., the enumerations are not processed in parallel like the lists in map, but instead any element from the first enumeration may appear as the first argument to f and any element from the second may appear as the second argument to f, etc.
If e is a one way enumeration, then the result is a one way enumeration and f-inv is ignored. Otherwise, the result is a two way enumeration.
Examples: | ||||||||||||||||||||||||||||||||||||||||||||||||
|
procedure
(pam/e f #:contract c e ...+) → one-way-enum?
f :
(dynamic->* #:mandatory-domain-contracts (map enum-contract e) #:range-contracts (list c)) c : contract? e : enum?
Examples: | ||||||||||
|
procedure
(except/e e [#:contract c] x ...) → two-way-enum?
e : two-way-enum? c : (or/c #f contract?) = #f x : (enum-contract e)
If c is #f, then it is not treated as a contract, instead the resulting contract is synthesized from contract on e and the xs.
Examples: | |||||||||||||
|
If the enumerations have overlapping elements, then pass #f as one-way-enum? so the result is a one way enumeration.
In more detail, if all of the arguments have or are two way enumerations and one-way-enum? is #f, then the result is also a two way enumeration and each argument must come with a predicate to distinguish its elements from the elements of the other enumerations. If the argument is a pair, then the predicate in the second position of the pair is used. If the argument is an enumeration, then it must be a flat enumeration and the contract is used as its predicate.
If any of the arguments are one way enumerations (or one-way-enum? is not #f), then the result is a one way enumeration and any predicates in the arguments are ignored.
Example: | ||||
|
procedure
(append/e [ #:one-way-enum? one-way-enum?] e-p ...+) → enum? one-way-enum? : boolean? = #f e-p : (or/c enum? (cons/c enum? (-> any/c boolean?)))
Like or/e the resulting enumeration is either a one way enumeration or a two way enumeration depending on the status of the arguments, and append/e has the same constraints on overlapping elements in the arguments.
Example: | ||||||
|
procedure
(thunk/e eth [ #:count count #:two-way-enum? is-two-way-enum? #:flat-enum? is-flat-enum?]) → enum?
eth :
(-> (and/c (if (= count +inf.0) infinite-enum? (and/c finite-enum? (let ([matching-count? (λ (e) (= (enum-count e) count))]) matching-count?))) (if is-two-way-enum? two-way-enum? one-way-enum?) (if is-flat-enum? flat-enum? (not/c flat-enum?)))) count : (or/c +inf.0 exact-nonnegative-integer?) = +inf.0 is-two-way-enum? : any/c = #t is-flat-enum? : any/c = #t
The count, is-two-way-enum?, and is-flat-enum? arguments must be accurate predications of the properties of the result of eth.
The argument eth is invoked when the result enumeration’s contract or bijection is used, either directly or indirectly via a call to enum-contract, from-nat, or to-nat.
Example: | |||||||
|
If ordering is 'square, it uses a generalized form of Szudzik’s “elegant” ordering and if ordering is 'diagonal, it uses a generalized form of Cantor’s mapping from pairs of naturals to naturals.
Examples: | ||||||||||||||||||||
|
procedure
(dep/e e f [ #:f-range-finite? f-range-finite? #:flat? flat? #:one-way? one-way?]) → enum? e : enum?
f :
(-> (enum-contract e) (and/c (if f-range-finite? finite-enum? infinite-enum?) (if one-way? one-way-enum? two-way-enum?) (if flat? flat-enum? (not/c flat-enum?)))) f-range-finite? : boolean? = #f flat? : boolean? = #t one-way? : boolean? = (one-way-enum? e)
Examples: | ||||||||||||||||||
|
procedure
(bounded-list/e k n)
→ (and/c finite-enum? two-way-enum? flat-enum?) k : exact-nonnegative-integer? n : exact-nonnegative-integer?
Example: | ||||
|
11.4 More Enumeration Operations
(require data/enumerate/lib) | |
package: data-enumerate-lib |
The data/enumerate/lib extends data/enumerate with a bunch of additional ways to build enumerations, some utility functions, and a bunch of pre-built enumerations.
11.4.1 More Enumeration Constructors
syntax
(cons/de [car-id car-enumeration-expr] [cdr-id (car-id) cdr-enumeration-expr] cons/de-option)
(cons/de [car-id (cdr-id) car-enumeration-expr] [cdr-id cdr-enumeration-expr] cons/de-option)
cons/de-option =
| #:dep-expression-finite? expr cons/de-option | #:flat? expr cons/de-option | #:one-way? expr cons/de-option
In the first form, the cdr-enumeration-expr can use car-id, which is bound to the value of the car position of the pair, mutatis mutandis in the second case.
If #:dep-expression-finite? keyword and expression are present, then the value of the dependent expression is expected to be an infinite enumeration if the expression evaluates to #f and a finite enumeration otherwise. If the keyword is not present, then the dependent expressions are expected to always produce infinite enumerations.
If #:flat? is present and evaluates to a true value, then the value of both sub-expressions are expected to be flat enumerations and if it evaluates to #f, then the enumerations must not be flat enumerations. If the keyword is not present, then the dependent expressions are expected to always produce flat enumerations.
If #:one-way? is present and evaluates to a true value, then the result enumeration is a one way enumeration
The dependent expressions are expected to always produce two way enumerations if the non-dependent expression is a two way enumeration and the dependent the dependent expressions are expected to always produce one way enumerations if the non-dependent expression is a one way enumeration.
Examples: | ||||||||||||||||||
|
procedure
(flip-dep/e e f [ #:f-range-finite? f-range-finite?] #:flat? flat? [ #:one-way? one-way?]) → enum? e : enum?
f :
(-> (enum-contract e) (and/c (if f-range-finite? finite-enum? infinite-enum?) (if one-way? one-way-enum? two-way-enum?) (if flat? flat-enum? (not/c flat-enum?)))) f-range-finite? : boolean? = #f flat? : #t one-way? : boolean? = (one-way-enum? e)
Examples: | |||||||||||||||||||
|
Examples: | ||||||||
|
procedure
(listof/e e [ #:simple-recursive? simple-recursive?]) → enum?
e :
(if simple-recursive? enum? infinite-enum?) simple-recursive? : any/c = #t
If simple-recursive? is #f, then the enumeration is constructed by first choosing a length and then using list/e to build lists of that length. If not, it builds a recursive enumeration using delay/e. The second option (which is the default) method is significantly more efficient when calling from-nat with large numbers, but it also has much shorter lists near the beginning of the enumeration.
Examples: | ||||||||
|
This plot shows some statistics for the first 500 items in each enumeration. The first plot shows how many different lengths each encounters. The red circles are when the #:simple-recursive? argument is #t and the blue stars are when that argument is #f.
This plot shows the different values, but this time on a log scale. As you can see, zero appears much more frequently when the #:simple-recursive? argument is #f.
procedure
(non-empty-listof/e e [ #:simple-recursive? simple-recursive?]) → enum?
e :
(if simple-recursive? enum? infinite-enum?) simple-recursive? : any/c = #t
Example: | ||
|
procedure
(listof-n/e e n) → enum?
e :
(if simple-recursive? enum? infinite-enum?) n : exact-nonnegative-integer?
Example: | ||||||||||||
|
syntax
(delay/e enum-expression keyword-options)
keyword-options =
| #:count count-expression keyword-options | #:two-way-enum? two-way-boolean-expression keyword-options | #:flat-enum? flat-boolean-expression keyword-options
If the count-expression is not supplied or if it evaluates to +inf.0, the resulting enumeration is a infinite enumeration. Otherwise the expression must evaluate to an exact-nonnegative-integer? and the resulting enumeration is a finite enumeration of the given count.
If two-way-boolean-expression is supplied and it evaluates to anything other than #f, the resulting enumeration must be a one way enumeration; otherwise it must be a two way enumeration.
If flat-boolean-expression is supplied and it evaluates to anything other than #f, the resulting enumeration must be a flat enumeration; otherwise it must not be.
Example: | ||||||
|
procedure
(take/e e n #:contract contract) → finite-enum?
e : enum?
n :
(if (finite-enum? e) (integer-in 0 (enum-count e)) exact-nonnegative-integer?)
contract :
(λ (x) (and ((enum-contract e) x) (< (to-nat e x) n)))
If the contract argument is not supplied, then e must be both a two way enumeration and a flat enumeration.
Example: | ||
|
procedure
(slice/e e lo hi #:contract contract) → finite-enum?
e : enum?
lo :
(and/c (if (finite-enum? e) (integer-in 0 (enum-count e)) exact-nonnegative-integer?) (<=/c hi))
hi :
(if (finite-enum? e) (integer-in 0 (enum-count e)) exact-nonnegative-integer?)
contract :
(and/c (enum-contract e) (λ (x) (<= lo (to-nat e x)) (< (to-nat e x) hi)))
Examples: | ||||
|
procedure
(fin/e x ...) → (and/c finite-enum? flat-enum?)
x : any/c
If there are multiple arguments, then they must all be distinct; numbers except for +nan.0 and +nan.f are compared using = and all others are compared using equal?).
If some other equality function is appropriate, use map/e with (below/e n) as the first argument to explicitly specify how to differentiate the elements of the enumeration.
Examples: | ||||
|
procedure
(single/e v #:equal? same?) → (and/c finite-enum? two-way-enum?)
v : any/c same? : equal?
It uses same? to build the contract in the enumeration, always passing v as the first argument to same?.
Examples: | ||||
|
procedure
(range/e lo hi) → (and/c two-way-enum? flat-enum?)
lo :
(and/c (or/c -inf.0 exact-integer?) (<=/c hi)) hi : (or/c exact-integer? +inf.0)
Examples: | ||||||||
|
procedure
(nat+/e lo) → (and/c infinite-enum? two-way-enum? flat-enum?)
lo : exact-nonnegative-integer?
Example: | ||
|
The ordering argument is the same as the one to list/e.
Example: | |||||||||||
|
procedure
→ (and/c finite-enum? two-way-enum? flat-enum?) n : exact-nonnegative-integer?
Example: | ||
|
procedure
(permutations/e l) → enum?
l : list?
Example: | ||||||||||||||||||||||||||
|
Examples: | ||||||||||||||||||||||||
|
procedure
e : finite-enum?
The infinite sequence corresponding to the natural number n is based on dividing the bits of (* (+ 1 n) pi) into chunks of bits where the largest value is (enum-count e). Since (* (+ 1 n) pi) has infinite digits, there are infinitely many such chunks. Since * is defined on all naturals, there are infinitely many such numbers. The generation of the sequence is efficient in the sense that the digits are generated incrementally without needing to go deeper than to find the requested value. The generation of the sequence is inefficient in the sense that the approximation of (* (+ 1 n) pi) gets larger and larger as you go deeper into the sequence.
Examples: | ||||||||||||||||||||||
|
procedure
(hash-traverse/e f xs #:get-contract get-contract) → enum? f : (-> any/c enum?) xs : (hash/c any/c any/c) get-contract : (-> any/c contract?)
The get-contract argument is applied to the keys in the hash and is expected to return the contract for the corresponding enumeration.
Examples: | ||||||||||||||||||||
|
procedure
(fold-enum f bs #:f-range-finite? f-range-finite?) → enum?
f :
(if f-range-finite? (-> list? any/c finite-enum?) (-> list? any/c infinite-enum?)) bs : list? f-range-finite? : #f
Examples: | ||||||||||||
|
11.4.2 Enumeration Utility
procedure
e : enum?
Examples: | ||||
|
11.4.3 Pre-built Enumerations
This section describes enumerations of some common Racket datatypes.
value
Examples: | ||||
|
value
Examples: | ||||
|
value
Example: | ||
|
value
Examples: | ||||
|
value
Example: | ||
|
value
Examples: | ||||||||||||||||
|
Example: | ||
|
Example: | ||
|
value
Example: | ||
|
Example: | ||
|
value
Example: | ||
|