Simple, Deterministic Dictionaries
(require data/ddict) | package: ddict |
This package defines immutable and mutable deterministic dictionaries (or ddicts, for short). A ddict is a dictionary (i.e. it implements the generic interface gen:dict) whose API mimics that of a hash table but which also guarantees LIFO ordering when iterating over the elements of the dictionary.
> (define dd (ddict 'A 0 'B 1 'C 2)) > dd #<ddict: '((C . 2) (B . 1) (A . 0))>
> "Note the ordering is LIFO w.r.t. the keys' insertion order" "Note the ordering is LIFO w.r.t. the keys' insertion order"
> (ddict->list (ddict-set dd 'Z 25)) '((Z . 25) (C . 2) (B . 1) (A . 0))
> (define mdd (for/mutable-ddict ([idx (in-naturals)] [name (in-list '(null eins zwei drei))]) (values idx name))) > mdd #<mutable-ddict: '((3 . drei) (2 . zwei) (1 . eins) (0 . null))>
> (ddict-set! mdd 4 'vier) > (ddict-set! mdd 6 'sechs) > (ddict-remove! mdd 1) > (ddict-remove! mdd 3)
> (for ([(key val) (in-ddict mdd)] [n (in-naturals)]) (printf "key ~a: ~a\n" n key) (printf "val ~a: ~a\n" n val))
key 0: 6
val 0: sechs
key 1: 4
val 1: vier
key 2: 2
val 2: zwei
key 3: 0
val 3: null
Caveats concerning concurrent modification: A mutable ddict can be manipulated with ddict-set!, ddict-remove!, ddict-ref!, and ddict-update! concurrently by multiple threads; updates to internal state are performed with a check-and-set operation which protects from invalid internal states. However, the following caveats apply:
Keys which are removed concurrently during the following operations may or may not be visited during the traversal: ddict-map, ddict-for-each, in-ddict in-ddict-keys, and in-ddict-values.
The ddict-update! and ddict-ref! functions perform a separate ddict-ref and ddict-set! when required as part of their functionality, which means that the update as a whole is not “atomic.”
Caveat concerning mutable keys: If a key in an equal?-based ddict is mutated, then the ddict’s behavior for insertion and lookup operations becomes unpredictable.
1 Constructors
procedure
(ddict key val ... ...) → (and/c immutable-ddict? ddict-equal?)
key : any/c val : any/c
procedure
(ddicteqv key val ... ...) → (and/c immutable-ddict? ddict-eqv?)
key : any/c val : any/c
procedure
(ddicteq key val ... ...) → (and/c immutable-ddict? ddict-eq?)
key : any/c val : any/c
procedure
(mutable-ddict key val ... ...)
→ (and/c mutable-ddict? ddict-equal?) key : any/c val : any/c
procedure
(mutable-ddicteqv key val ... ...)
→ (and/c mutable-ddict? ddict-eqv?) key : any/c val : any/c
procedure
(mutable-ddicteq key val ... ...)
→ (and/c mutable-ddict? ddict-eq?) key : any/c val : any/c
The key to val mappings are added to the table in the order they appear in the argument list, so later mappings can hide earlier ones if the keys are equal.
procedure
(make-ddict [assocs]) → (and/c immutable-ddict? ddict-equal?)
assocs : (listof pair?) = null
procedure
(make-ddicteqv [assocs]) → (and/c immutable-ddict? ddict-eqv?)
assocs : (listof pair?) = null
procedure
(make-ddicteq [assocs]) → (and/c immutable-ddict? ddict-eq?)
assocs : (listof pair?) = null
procedure
(make-mutable-ddict [assocs])
→ (and/c mutable-ddict? ddict-equal?) assocs : (listof pair?) = null
procedure
(make-mutable-ddicteqv [assocs])
→ (and/c mutable-ddict? ddict-eqv?) assocs : (listof pair?) = null
procedure
(make-mutable-ddicteq [assocs])
→ (and/c mutable-ddict? ddict-eq?) assocs : (listof pair?) = null
2 Basic Predicates
procedure
(immutable-ddict? v) → boolean?
v : any/c
procedure
(mutable-ddict? v) → boolean?
v : any/c
procedure
(ddict-equal? dd) → boolean?
dd : ddict?
procedure
(ddict-eqv? dd) → boolean?
dd : ddict?
procedure
dd : ddict?
ddict-eqv? returns #t if the given ddict’s keys are compared with eqv?, #f otherwise.
ddict-eq? returns #t if the given ddict’s keys are compared with eq?, #f otherwise.
3 Basic Operations
procedure
(ddict-set dd key val) → immutable-ddict?
dd : immutable-ddict? key : any/c val : any/c
See also the caveat concerning mutable keys above.
procedure
(ddict-set! dd key val) → void?
dd : mutable-ddict? key : any/c val : any/c
See also the caveats concerning concurrent modification and the caveat concerning mutable keys above.
procedure
dd : ddict? key : any/c
failure-result : (failure-result/c any/c) = (λ () (raise (exn:fail:contract ....)))
If failure-result is a procedure, it is called with no arguments to produce the result.
Otherwise, failure-result is returned as the result.
See also the caveat concerning mutable keys above.
procedure
(ddict-has-key? dd key) → boolean?
dd : ddict? key : any/c
procedure
(ddict-remove dd key) → immutable-ddict?
dd : immutable-ddict? key : any/c
See also the caveat concerning mutable keys above.
procedure
(ddict-remove! dd key) → void?
dd : mutable-ddict? key : any/c
See also the caveats concerning concurrent modification and the caveat concerning mutable keys above.
procedure
dd : ddict?
procedure
(ddict-empty? dd) → boolean?
dd : ddict?
4 Additional Operations
procedure
(ddict-set* dd key val ... ...) → immutable-ddict?
dd : immutable-ddict? key : any/c val : any/c
procedure
(ddict-set*! dd key val ... ...) → void?
dd : mutable-ddict? key : any/c val : any/c
procedure
(ddict-ref! dd key to-set) → any/c
dd : mutable-ddict? key : any/c to-set : any/c
See also the caveats concerning concurrent modification and the caveat concerning mutable keys above.
procedure
(ddict-update dd key updater [failure-result]) → any/c
dd : immutable-ddict? key : any/c updater : (any/c . -> . any/c)
failure-result : (failure-result/c any/c) = (λ () (raise (exn:fail:contract ....)))
See also the caveat concerning mutable keys above.
procedure
(ddict-update! dd key updater [failure-result]) → void?
dd : mutable-ddict? key : any/c updater : (any/c . -> . any/c)
failure-result : (failure-result/c any/c) = (λ () (raise (exn:fail:contract ....)))
See also the caveats concerning concurrent modification and the caveat concerning mutable keys above.
procedure
(ddict-keys-subset? dd1 dd2) → boolean?
dd1 : ddict? dd2 : ddict?
procedure
(ddict-clear! dd) → void?
dd : mutable-ddict?
See also the caveats concerning concurrent modification and the caveat concerning mutable keys above.
procedure
(ddict-clear dd) → immutable-ddict?
dd : immutable-ddict?
procedure
(ddict-copy dd) → mutable-ddict?
dd : ddict?
procedure
(ddict-copy-clear dd) → ddict?
dd : ddict?
5 Traversal and Iteration Functions
procedure
(ddict-keys dd) → (listof any/c)
dd : ddict?
procedure
(ddict-values dd) → (listof any/c)
dd : ddict?
procedure
(ddict-position? v) → boolean?
v : any/c
procedure
dd : ddict?
procedure
(ddict-iterate-next dd pos) → ddict-position?
dd : ddict? pos : ddict-position?
procedure
(ddict-iterate-key dd pos) → any/c
dd : ddict? pos : ddict-position?
procedure
(ddict-iterate-value dd pos) → any/c
dd : ddict? pos : ddict-position?
procedure
dd : ddict?
procedure
(in-ddict-keys dd) → sequence?
dd : ddict?
procedure
(in-ddict-values dd) → sequence?
dd : ddict?
in-ddict-keys returns a sequence containing the keys of dd in LIFO order w.r.t. the order they were inserted into dd.
in-ddict-values returns a sequence containing the values of dd in LIFO order w.r.t. the order their keys were inserted into dd.
These forms provide for fast, direct iteration when used in conjunction with Racket’s various for loops.
syntax
(for/ddict (for-clause ...) body-or-break ... body)
syntax
(for/ddicteqv (for-clause ...) body-or-break ... body)
syntax
(for/ddicteq (for-clause ...) body-or-break ... body)
syntax
(for*/ddict (for-clause ...) body-or-break ... body)
syntax
(for*/ddicteqv (for-clause ...) body-or-break ... body)
syntax
(for*/ddicteq (for-clause ...) body-or-break ... body)
syntax
(for/mutable-ddict (for-clause ...) body-or-break ... body)
syntax
(for/mutable-ddicteqv (for-clause ...) body-or-break ... body)
syntax
(for/mutable-ddicteq (for-clause ...) body-or-break ... body)
syntax
(for*/mutable-ddict (for-clause ...) body-or-break ... body)
syntax
(for*/mutable-ddicteqv (for-clause ...) body-or-break ... body)
syntax
(for*/mutable-ddicteq (for-clause ...) body-or-break ... body)
6 Performance and Memory Usage
Performance. Immutable and mutable ddicts internally use Racket’s immutable hash data structure along with a list of keys in order to provide hash-like performance and a deterministic iteration order. ddict operations obviously have overhead which the native hash operations do not, but as a dictionary increases in size the overhead should become less noticeable (i.e. the overhead is constant for the atomic ddict operations so the asymptotic complexity is unaffected). To determine if this overhead is acceptable you should of course perform your own application-specific benchmarks.
Memory Usage. In order to keep ddict operations such as ddict-remove efficient (i.e. non-linear), we do not immediately remove elements from the internal key list. Instead, a certain amount of “fragmentation” in the key list is tolerated, but once it passes a predetermined threshold (when the number of removed key slots exceeds the number of active keys), the list is then “defragmented”. To prevent this fragmentation from causing unexpected memory leaks, each key in the key list is stored in a weak-box so its presence in the key list after removal does not prevent garbage collection that would otherwise occur.
The hope is that these implementation details are mostly unobservable to ddict users since their costs will be amortized.
If a user is concerned and wants more fine grained control over the presence of internal fragmentation (e.g. if removals are performed early on in computation then never again) the following functions report on the presence of fragmentation and allow a user to remove any:
procedure
(ddict-compact? dd) → boolean?
dd : ddict?
procedure
(ddict-compact dd) → immutable-ddict?
dd : immutable-ddict?
procedure
(ddict-compact! dd) → void?
dd : mutable-ddict?