RaList: Purely Functional Random-Access Lists
David Van Horn <dvanhorn@cs.umd.edu>
Random-access lists are a purely functional data structure for representing lists of values. A random-access list may act as a drop in replacement for the usual sequential list data structure (cons?, cons, car, cdr), which additionally supports fast index-based addressing and updating (list-ref, list-set).
This document outlines the API for the random-access list library. This implementation is based on Okasaki, FPCA ’95.
1 Random-Access Lists
(require data/ralist) | package: ralist |
In addition to data/ralist, there is a data/ralist/contract module that provides the same bindings but with checked contracts. The only difference is performance and error checking. See Checked and Unchecked contracts for details.
> (list 0 1 2 3) (0 1 2 3)
> (cons 1 2) (1 . 2)
> (car (cons 1 2)) 1
> (cdr (cons 1 2)) 2
> (map (lambda (i) (/ 1 i)) (list 1 2 3)) (1 1/2 1/3)
> (list-ref (list 'x 'y 'z) 2) 'z
> (list-set (list 'x 'y 'z) 2 'q) (x y q)
This can have a big impact on efficiency of accessing elements in a list by their index. For example, getting the last element of a sequential list takes time proportional to the length of the list, but can be done much faster with random-access lists:
> (module m racket/base (require data/ralist) (define ls (build-list 100000 values)) (time (for ([i 1000]) (last ls)))) > (require 'm) cpu time: 1 real time: 1 gc time: 0
> (module n racket/base (require racket/list) (define ls (build-list 100000 values)) (time (for ([i 1000]) (last ls)))) > (require 'n) cpu time: 402 real time: 403 gc time: 0
> (module o racket/base (require data/ralist) (define ls (build-list 100000 values)) (define (last xs) (if (empty? (cdr xs)) (car xs) (last (cdr xs)))) (time (for ([i 1000]) (last ls)))) > (require 'o) cpu time: 7072 real time: 7080 gc time: 56
> (module p racket/base (require racket/list) (define ls (build-list 100000 values)) (define (last xs) (if (empty? (cdr xs)) (car xs) (last (cdr xs)))) (time (for ([i 1000]) (last ls)))) > (require 'p) cpu time: 390 real time: 390 gc time: 0
There are several benchmarks described in the Benchmarks section comparing the performance of random-access lists to sequential-access lists as well as mutable data structures such as vectors. In general, random-access lists are well-suited for situations requiring variable-length lists that are mostly accessed by indexed.
1.1 Pairs and Lists
A pair combines exactly two values. The first value is accessed with the car procedure, and the second value is accessed with the cdr procedure. Pairs are not mutable.
A list is recursively defined: it is either the constant empty, or it is a pair whose second value is a list.
A list can be used as a single-valued sequence (see Sequences). The elements of the list serve as elements of the sequence. See also in-list.
1.2 Sequences
Random-access lists implement the sequence interface, so (list? x) implies (sequence? x), and elements of a list may be extracted with any of the for syntactic forms.
> (in-list (list 1 2 3)) (1 2 3)
> (for/fold ([sum 0] [rev-roots empty]) ([i (in-list (list 1 2 3 4))]) (values (+ sum i) (cons (sqrt i) rev-roots)))
10
(2 1.7320508075688772 1.4142135623730951 1)
1.3 Iterations and Comprehensions
syntax
(for/list ([id sequence-expr] ...) body ...+)
> (for/list ([i '(1 2 3)] [j "abc"] #:when (odd? i) [k #(#t #f)]) (list i j k)) ((1 #\a #t) (1 #\a #f) (3 #\c #t) (3 #\c #f))
> (for/list () 'any) (any)
> (for/list ([i '()]) (error "doesn't get here")) '()
1.4 Values
> empty '()
> (first+rest (list 1 2 3))
1
(2 3)
> (first+rest empty) first+rest: contract violation
expected: ra:cons?
given: '()
in: an and/c case of
the 1st argument of
(-> (and/c ra:cons? ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:48.2
> (first+rest (cons 1 2)) first+rest: contract violation
expected: ra:list?
given: (1 . 2)
in: an and/c case of
the 1st argument of
(-> (and/c ra:cons? ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:48.2
> (first+rest empty) ra:first+rest: expected cons, given: ()
> (first+rest (cons 1 2))
1
2
> (first empty) first: contract violation
expected: ra:cons?
given: '()
in: an and/c case of
the 1st argument of
(-> (and/c ra:cons? ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:49.2
> (first (cons 'x 'y)) first: contract violation
expected: ra:list?
given: (x . y)
in: an and/c case of
the 1st argument of
(-> (and/c ra:cons? ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:49.2
> (rest empty) rest: contract violation
expected: ra:cons?
given: '()
in: an and/c case of
the 1st argument of
(-> (and/c ra:cons? ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:50.2
> (rest (cons 'x 'y)) rest: contract violation
expected: ra:list?
given: (x . y)
in: an and/c case of
the 1st argument of
(-> (and/c ra:cons? ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:50.2
> (car+cdr empty) car+cdr: contract violation
expected: ra:cons?
given: '()
in: the 1st argument of
(-> ra:cons? any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:51.2
> (car empty) car: contract violation
expected: ra:cons?
given: '()
in: the 1st argument of
(-> ra:cons? any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:52.2
> (cdr empty) cdr: contract violation
expected: ra:cons?
given: '()
in: the 1st argument of
(-> ra:cons? any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:53.2
procedure
xs : (count>/c i) i : natural-number/c
> (list-ref (list 'x 'y 'z) 3) list-ref: contract violation
expected: (count>/c 3)
given: (x y z)
in: the ls argument of
(->i
((ls (i) (count>/c i))
(i natural-number/c))
any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:81.2
> (list-ref (list* 'x 'y 'z) 2) list-ref: contract violation
expected: (count>/c 2)
given: (x y . z)
in: the ls argument of
(->i
((ls (i) (count>/c i))
(i natural-number/c))
any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:81.2
> (list-ref (list 'x 'y 'z) 3) kons-size: contract violation
expected: kons?
given: '()
> (list-ref (list* 'x 'y 'z) 2) kons-size: contract violation
expected: kons?
given: 'z
procedure
xs : (count>/c i) i : natural-number/c x : any/c
> (list-set (list 'x 'y 'z) 3 'd) list-set: contract violation
expected: (count>/c 3)
given: (x y z)
in: the ls argument of
(->i
((ls (i) (count>/c i))
(i natural-number/c)
(x any/c))
any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:85.2
> (list-set (list* 'x 'y 'z) 2 'c) list-set: contract violation
expected: (count>/c 2)
given: (x y . z)
in: the ls argument of
(->i
((ls (i) (count>/c i))
(i natural-number/c)
(x any/c))
any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:85.2
> (list-set (list 'x 'y 'z) 3 'd) kons-size: contract violation
expected: kons?
given: '()
> (list-set (list* 'x 'y 'z) 2 'c) kons-size: contract violation
expected: kons?
given: 'z
procedure
(list-update xs i f) → cons?
xs : (count>/c i) i : natural-number/c f : (procedure-arity-includes/c 1)
procedure
(list-ref/set xs i v) →
any/c cons? xs : (count>/c i) i : natural-number/c v : any/c
procedure
(list-ref/update xs i f) →
any/c cons? xs : (count>/c i) i : natural-number/c f : (procedure-arity-includes/c 1)
> (second (list 'a 'b)) 'b
> (third (list 'a 'b 'c)) 'c
> (fourth (list 'a 'b 'c 'd)) 'd
> (fifth (list 'a 'b 'c 'd 'e)) 'e
> (sixth (list 'a 'b 'c 'd 'e 'f)) 'f
> (seventh (list 'a 'b 'c 'd 'e 'f 'g)) 'g
> (eighth (list 'a 'b 'c 'd 'e 'f 'g 'h)) 'h
> (ninth (list 'a 'b 'c 'd 'e 'f 'g 'h 'i)) 'i
> (tenth (list 'a 'b 'c 'd 'e 'f 'g 'h 'i 'j)) 'j
> (second (list* 'a 'b 'c)) second: contract violation
expected: ra:list?
given: (a b . c)
in: an and/c case of
the 1st argument of
(-> (and/c ra:list? (count>/c 1)) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:64.2
> (second (list 'a)) second: contract violation
expected: (count>/c 1)
given: (a)
in: an and/c case of
the 1st argument of
(-> (and/c ra:list? (count>/c 1)) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:64.2
> (second (list* 'a 'b 'c)) 'b
> (second (list 'a)) kons-size: contract violation
expected: kons?
given: '()
> (last empty) last: contract violation
expected: ra:cons?
given: '()
in: an and/c case of
the 1st argument of
(-> (and/c ra:cons? ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:73.2
> (last (list* 1 2 3)) last: contract violation
expected: ra:list?
given: (1 2 . 3)
in: an and/c case of
the 1st argument of
(-> (and/c ra:cons? ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:73.2
procedure
f :
(or/c (is-true/c (zero? (count xs))) (procedure-arity-includes/c (add1 (mz:length ...)))) xs : (and/c list? (count=/c (count xs)))
> (map add1 (list 1 2 3) (list 4 5 6)) map: contract violation
expected: (or/c (is-true/c (zero? (count xs)))
(procedure-arity-includes/c 2))
given: #<procedure:add1>
in: the f argument of
(->i
((f
(xs r)
(or/c
(is-true/c (zero? (count xs)))
(procedure-arity-includes/c
(add1 (mz:length r)))))
(xs ra:list?))
#:rest
(r
(xs)
(listof
(and/c list? (count=/c (count xs)))))
any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:114.2
> (map + (list 1 2 3) (list 4)) map: contract violation
expected: (listof (and/c ra:list? (count=/c 3)))
given: '((4))
in: the r argument of
(->i
((f
(xs r)
(or/c
(is-true/c (zero? (count xs)))
(procedure-arity-includes/c
(add1 (mz:length r)))))
(xs ra:list?))
#:rest
(r
(xs)
(listof
(and/c list? (count=/c (count xs)))))
any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:114.2
> (map add1 (list 1 2 3) (list 4 5 6)) add1: arity mismatch;
the expected number of arguments does not match the given
number
expected: 1
given: 2
arguments...:
1
4
> (map + (list 1 2 3) (list 4)) +: contract violation
expected: number?
given: '#s(node 1 2 3)
argument position: 1st
other arguments...:
4
procedure
f :
(or/c (is-true/c (zero? (count xs))) (procedure-arity-includes/c (+ 2 (mz:length ...)))) b : any/c xs : list?
procedure
f :
(or/c (is-true/c (zero? (count xs))) (procedure-arity-includes/c (+ 2 (mz:length ...)))) a : any/c xs : list?
procedure
f :
(or/c (is-true/c (zero? (count xs))) (procedure-arity-includes/c (add1 (mz:length ...)))) xs : (and/c list? (count=/c (count xs)))
procedure
f :
(or/c (is-true/c (zero? (count xs))) (procedure-arity-includes/c (add1 (mz:length ...)))) xs : (and/c list? (count=/c (count xs)))
procedure
n : natural-number/c x : any/c
procedure
(build-list n f) → list?
n : natural-number/c
f :
(or/c (is-true/c (zero? n)) (procedure-arity-includes/c 1))
> (build-list 0 'not-function) '()
> (build-list 0 (lambda (i) i)) '()
> (build-list 10 values) (0 1 2 3 4 5 6 7 8 9)
> (build-list 5 (lambda (x) (* x x))) (0 1 4 9 16)
> (build-list 5 'not-function) build-list: contract violation
expected: (or/c (is-true/c (zero? n))
(procedure-arity-includes/c 1))
given: 'not-function
in: the f argument of
(->i
((n natural-number/c)
(f
(n)
(or/c
(is-true/c (zero? n))
(procedure-arity-includes/c 1))))
any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:108.2
> (build-list 5 'not-function) application: not a procedure;
expected a procedure that can be applied to arguments
given: 'not-function
arguments...:
2
procedure
(length xs) → natural-number/c
xs : list?
> (length (list* 1 2 3)) length: contract violation
expected: ra:list?
given: (1 2 . 3)
in: the 1st argument of
(-> ra:list? any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:60.2
procedure
(count xs) → natural-number/c
xs : any/c
procedure
xs : list? i : natural-number/c
> (append 3 5) append: contract violation
expected: ra:list?
given: 3
in: an element of
the rest argument of
(->* () #:rest (listof ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:61.2
> (append 1 (list 2 3)) append: contract violation
expected: ra:list?
given: 1
in: an element of
the rest argument of
(->* () #:rest (listof ra:list?) any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:61.2
> (append 3 5) ra:first+rest: expected cons, given: 3
> (append 1 (list 2 3)) ra:first+rest: expected cons, given: 1
> (reverse (list* 1 2 3)) reverse: contract violation
expected: ra:list?
given: (1 2 . 3)
in: the 1st argument of
(-> ra:list? any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:62.2
procedure
n : natural-number/c
> (range (list 1 2 3)) range: contract violation
expected: natural-number/c
given: (1 2 3)
in: the 1st argument of
(-> natural-number/c any)
contract from:
<pkgs>/ralist/data/ralist/contract.rkt
blaming: top-level
(assuming the contract is correct)
at: <pkgs>/ralist/data/ralist/contract.rkt:63.2
> (range (list 1 2 3)) arithmetic-shift: contract violation
expected: exact-integer?
given: (1 2 3)
argument position: 1st
other arguments...:
-1
1.5 Checked and Unchecked contracts
This library provides bindings for list operations in two forms: the first assumes all contracts are met, the second checks.
The observable differences are in their failure modes—
The checked bindings are designed to be complete in their checking of contract properties, regardless of computational expense. The unchecked bindings are designed to be fast and to give reasonable error messages to any violation that can easily be detected. Given inputs that satisfy the contract, both versions produced equivalent results.
The main module provides bindings for list operations with unchecked contracts. Where appropriate, examples are given demonstrating the differences in failure modes for the checked and unchecked bindings. See the benchmark on Contracted vs. Uncontracted bindings for a performance comparison.
2 Contracts
(require data/ralist/contract) | package: ralist |
procedure
(count=/c n) → flat-contract?
n : natural-number/c
procedure
(count>/c n) → flat-contract?
n : natural-number/c
procedure
(is-true/c x) → flat-contract?
x : any/c
3 Tests
(require data/ralist/run-tests) | package: ralist |
Runs all unit tests for this package.
3.1 Tree tests
(require data/ralist/tests/tree) | package: ralist |
value
> (run-tests tree-tests) 13 success(es) 0 failure(s) 0 error(s) 13 test(s) run
0
3.2 RaList tests
(require data/ralist/tests/ra-list) | package: ralist |
value
> (run-tests ra-list-tests) 439 success(es) 0 failure(s) 0 error(s) 439 test(s) run
0
3.3 Garden fence tests
(require data/ralist/tests/garden-fence) | |
package: ralist |
value
> (run-tests garden-fence-tests) 80 success(es) 0 failure(s) 0 error(s) 80 test(s) run
0
3.4 Frequency counting tests
(require data/ralist/tests/freq-count) | package: ralist |
value
> (run-tests freq-count-tests) 5 success(es) 0 failure(s) 0 error(s) 5 test(s) run
0
4 Benchmarks
(require data/ralist/run-benchmarks) | package: ralist |
Runs all of the benchmarks for this package.
4.1 Random-access vs. Sequential-access lists
(require data/ralist/benchmarks/ra-list) | |
package: ralist |
This benchmark compares the performance of typical list operations for random and sequential lists.
procedure
(run-ra-list-benchmark) → void?
4.2 Contracted vs. Uncontracted bindings
(require data/ralist/benchmarks/contract) | |
package: ralist |
This benchmark compares the performance of the contracted and uncontracted bindings.
procedure
(run-contract-benchmark) → void?
4.3 Frequency counting
(require data/ralist/benchmarks/freq-count) | |
package: ralist |
This benchmark compares an number of imperative and functional solutions to the problem of counting the frequencies of each number in a given list of numbers.
See the thread starting here for discussion.
procedure
(run-freq-count-benchmark) → void?
4.4 Garden fence encryption
(require data/ralist/benchmarks/garden-fence) | |
package: ralist |
This benchmark compares solutions to the problem of garden fence encryption.
Garden fence encryption works as follows: you are given a plain text message (String) and a key (Nat). You scramble the message by a process that depends on the given key, producing a cipher text message (String) of the same length as the given plain text message. The scrambled message can be de-scrambled to obtain the original message by an inverse process when it is given the same key.
procedure
(encrypt s k) → string?
s : string? k : natural-number/c
procedure
(decrypt s k) → string?
s : string? k : natural-number/c
;;;; 1. d k = (d k) = "dk" |
;;;; 2. i n l = (i n l) = "inl" |
;;;; 3. e i a = (e i a) = "eia" |
;;;; 4. s e r t = (s e r t) = "sert" |
;;;; 5. i t t x = (i t t x) = "ittx" |
;;;; 6. s e = (s e) = "se" |
An imperative, vector-based algorithm.
A functional translation of the above using random-access lists.
A functional algorithm designed by output structure.
A combinator style algorithm.
A cyclic sequence algorithm.
See the thread starting here and here for discussion.
procedure
(run-garden-fence-benchmark) → void?