Lists

Provide efficient access to the front element, and can efficiently add and remove elements from the front. Sometimes also called a "stack." Primary operations:

  • first(xs): get the first element of xs
  • popfirst(xs): returns a new list that looks like xs but with the first element removed
  • pushfirst(xs, x): returns a new list with x added to the front of xs. The infix operator (pronounced \rightharpoonup) is often more convenient. Note that it is right-associative, so x ⇀ y ⇀ zs is equivalent to pushfirst(pushfirst(zs, y), x)

Additionally, PureFun.jl implements default implementations of a variety of Abstract Vector-like methods for list types, though they are not necessarily efficient. All of these functions have similar meanings to their mutating (with a ! at the end of the function name) counterparts in Base. When these functions are already present in StaticArrays.jl, PureFun.jl just adds methods to the existing functions.

  • reverse
  • insert
  • getindex (xs[i] to get the i-th element of xs)
  • setindex (help wanted: nice syntax for non-mutating setindex)
  • append (use the infix notation xs ⧺ ys to append ys to the end of xs)

Lists iterate in LIFO order, and work with a variety of built-in higher-order functions:

  • eager versions of map, filter, accumulate
  • (map)foldl, (map)foldr, (map)reduce

All of the lists types provide the same functionality and interface, but different implementations are optimized for different use-cases and types of operations. For example, getting or setting an index in a linked list usually takes $\mathcal{O}(n)$ time, but PureFun.RandomAccess.List provides indexing operations that take $\mathcal{O}(\log{_2}n)$. All of the list implementations in PureFun.jl inherit from the abstract type PureFun.PFList. Complexities presented below are worst-case, unless stated otherwise.

Linked.List ($\S{2.1}$)

PureFun.Linked.ListType
Linked.List{T}()
Linked.List(iter)

The Linked.List ($\S{2.1}$) is the simplest of the list types, and the fastest for the primary operations, which are all $\mathcal{O}(1)$.

Examples

julia> using PureFun, PureFun.Linked
julia> l = Linked.List(1:3)
3-element PureFun.Linked.NonEmpty{Int64}
1
2
3


julia> m = pushfirst(l, 10)
4-element PureFun.Linked.NonEmpty{Int64}
10
1
2
3


julia> first(l)
1

julia> first(m)
10

julia> popfirst(m) == l
true
source

RandomAccess.List ($\S{9.3.1}$)

PureFun.RandomAccess.ListType
RandomAccess.List{T}()
RandomAccess.List(iter)

A RandomAccess.List ($\S{9.3.2}$) adds efficient ($\mathcal{O}(\log{}n)$) indexing (getindex and setindex) operations to the $\mathcal{O}(1)$ primary operations. The implementation stores elements in complete binary trees representing digits in the skew binary number system, as described in this blog post.

Examples

julia> rl = PureFun.RandomAccess.List(1:1_000)
1
2
3
4
5
6
7
...


julia> rl[937]
937
source

Catenable.List ($\S{10.2.1}$)

PureFun.Catenable.ListType
Catenable.List{T}()
Catenable.List(iter)

A Catenable.List ($\S{10.2.1}$) supports the usual list operations, but unlike the Linked.List you can append two catenable lists in constant time. These lists are presented in section 10.2.1 of the book, as an example of data-structural bootstrapping. In addition to list functions, catenable lists also support push. Catenable lists work by maintaining the head element plus a queue of catenable lists. Each element of this queue is suspended. first takes constant time, while pushfirst, popfirst, push, and require amortized (rather than worst-case) constant time.

Examples

julia> a = PureFun.Catenable.List(1:3);

julia> b = PureFun.Catenable.List(4:5);

julia> a ⧺ b
1
2
7
4
5
source

VectorCopy.List: an immutable wrapper for Base.Vector

PureFun.VectorCopy.ListType
VectorCopy.List{T}()
VectorCopy.List(iter)

VectorCopy.List is a wrapper around Base.Vector with copy-on-write semantics. pushfirst is $\mathcal{O}(n)$, but iteration and indexing are very fast. Useful for small lists, or for lists that are traversed frequently relative to how often they are modified.

source

CPU-cache friendly lists: Chunky.@list

Pointer-based data structures are at a disadvantage performance-wise when compared to arrays and vectors. Memory accesses are high-latency operations, so that observed performance will be determined by the number of cache misses regardless of the on-paper complexity guarantees of an algorithm. The VectorCopy.List gets around this issue by storing adjacent list values physically close to each other in contiguous memory, but write operations require allocating $\mathcal{O}(n)$ memory, which quickly becomes prohibitive. The unrolled linked list strikes a compromise between the two extremes by storing chunks of values together in each list cell. PureFun.Chunky.@list converts any list type to a "chunky" version, using one of the chunk types provided by PureFun.Contiguous:

PureFun.Chunky.@listMacro
Chunky.@list Name ListType ChunkType

Creates a new list type (implements all list functions and inherits from PureFun.PFList) by assembling a list (of type ListType) of chunks (of type ChunkType). Assuming ChunkType stores chunk elements contiguously, the resulting list will have improved iteration performance. PureFun.Contiguous.VectorChunk and PureFun.Contiguous.StaticChunk implement the chunk type and can be used in chunky lists.

Examples

This example creates a chunky list called ChunkyList consisting of (up to) 16-element chunks stored contiguously in memory as static arrays, linked together via a linked list. The resulting list has the same interface as any other list type in PureFun.jl:

julia> using PureFun, PureFun.Linked, PureFun.Chunky, PureFun.Contiguous

julia> Chunky.@list CList list=Linked.List chunk=Contiguous.StaticChunk{16}

julia> clist = CList(1:100)
100-element CList{Int64}
1
2
3
4
5
6
7
...

julia> clist[18]
18

julia> mapfoldl(sqrt, +, clist)
671.4629471031477

Similarly, the following example uses PureFun.RandomAccess.Lists and chunks of Base.Vector:

julia> using PureFun, PureFun.RandomAccess, PureFun.Chunky, PureFun.Contiguous

julia> Chunky.@list CRList list=RandomAccess.List chunk=Contiguous.VectorChunk{256}

julia> em = CRList{Float64}()
0-element CRList{Float64}


julia> 1.0 ⇀ 2.0 ⇀ em
2-element CRList{Float64}
1.0
2.0
source

For more on cache-friendly data structures and the role of cache misses on performance:

Custom double-ended queue: $\S{5.2}$ (excercise 5.1)

PureFun.Batched.@dequeMacro
Batched.@deque Name ListType

Deques are like lists but with symmetric efficient operations on the front (pushfirst, popfirst, first) and the back (push, pop, last). The Batched.@deque functor takes any existing list implementation (ListType), and makes it double-ended. The Batched.@deque works by batching occasional reversals (which are $\mathcal{O}(n)$) so that all operations require amortized constant time.

Examples

julia> Batched.@deque Deque PureFun.Linked.List

julia> d = Deque{Int}()
0-element Deque{Int64}


julia> 1 ⇀ 2 ⇀ 3 ⇀ d
3-element Deque{Int64}
1
2
3


julia> alpha = Deque('a':'z')
26-element Deque{Char}
a
b
c
d
e
f
g
...

julia> first(alpha), last(alpha)
('a', 'z')

julia> alpha |> pop |> last
'y': ASCII/Unicode U+0079 (category Ll: Letter, lowercase)

julia> alpha |> popfirst |> first
'b': ASCII/Unicode U+0062 (category Ll: Letter, lowercase)
source

Function reference

StaticArrays.pushfirstMethod
cons(x, xs::PFList)
pushfirst(xs::PFList, x)
x ⇀ xs

Return the PFList that results from adding x to the front of xs.

source
PureFun.appendFunction
append(xs, ys)
xs ⧺ ys

Concatenate two PFLists.

julia> l1 = PureFun.Linked.List(1:3);

julia> l2 = PureFun.Linked.List(4:6);

julia> l1 ⧺ l2
1
2
3
4
5
6
source
Base.setindexMethod
setindex(l::PFList, newval, ind)

Return a new list with the value at index ind set to newval

Examples

julia> using PureFun, PureFun.RandomAccess

julia> l = RandomAccess.List(1:10)
10-element PureFun.RandomAccess.List{Int64}
1
2
3
4
5
6
7
...

julia> setindex(l, 99, 4)
10-element PureFun.RandomAccess.List{Int64}
1
2
3
99
5
6
7
...
source
PureFun.halfishFunction
halfish(xs)

Split xs roughly in half, and return the two halves as a tuple (front, back).

Examples

julia< using PureFun
julia> l = PureFun.Linked.List(1:100)
100-element PureFun.Linked.NonEmpty{Int64}
1
2
3
4
5
6
7
...

julia> halves = halfish(l)
(1, 2, 3, 4, 5, ..., 51, 52, 53, 54, 55, ...)

julia> length(halves[1]), length(halves[2])
(50, 50)

julia> halves[2]
50-element PureFun.Linked.NonEmpty{Int64}
51
52
53
54
55
56
57
...
source