Design

This document looks at implementation of the same adjacency list data structure that is used in Boost.Graph. It is a string over vertex storage, where each vertex stores a string of neighbors. The vertices aren’t necessarily indexed in any way. An adjacency list knows which entry is a neighbor of which other entry, but the string of vertices and string of neighbors may be stored many different ways.

Vertex Descriptor

The vertex_descriptor in Boost.Graph is an opaque handle to a vertex. It is usually a pointer to the vertex, except in one special case. For adjacency lists constructed with vector types for both the vertex list and neighbor lists, the vertex_descriptor is an integer.

Instead of using a dictionary at vertices and edges to store properties, it is possible to append those properties to the vertex type and neighbor type. In the following example, note that the type of the VertexDescriptor used in the Neighbor class is determined by the Vertex class. It’s recursive.:

immutable type Neighbor{VertexDescriptor,EdgeProperty}
  n::VertexDescriptor
  edge_property::EdgeProperty
end

immutable type Vertex{VertexProperty,EdgeProperty}
  # This is the recursive bit, where it makes pointers to itself.
  v::Deque{Neighbor{Vertex{VertexProperty,EdgeProperty},EdgeProperty}
  vertex_property::VertexProperty
end

type AdjacencyList{VertexProperty,EdgeProperty}
  vertices::Deque{Vertex{VertexProperty,EdgeProperty}}
  is_directed::Bool
end

This setup would work fine for the Deque type, but Boost.Graph uses C++ vector, list, or set types. An array type would use integers for indexing.:

immutable type Neighbor{VertexDescriptor,EdgeProperty}
  n::VertexDescriptor
  edge_property::EdgeProperty
end

immutable type Vertex{VertexProperty,EdgeProperty}
  # This is the recursive bit, where it makes pointers to itself.
  v::Vector{Neighbor{Int,EdgeProperty}
  vertex_property::VertexProperty
end

type AdjacencyList{VertexProperty,EdgeProperty}
  vertices::Vector{Vertex{VertexProperty,EdgeProperty}}
  is_directed::Bool
end

Parameterizing Storage

It is difficult in Julia to create a type which changes storage class depending on a parameter type. For instance, this is not allowed:

type Foo{C,V}
  container::C{V}
end

That it is not allowed is specified in an issue 3359, https://github.com/JuliaLang/julia/issues/3359. Although it is not allowed, this works (for now):

cconstruct(x::TypeVar, y)=x
cconstruct(x::Union(DataType,TypeConstructor), y)=x{y}

type Foo{C,V}
  container::cconstruct(C,V)
end

Foo{Vector,Int}(Array(Int, 0))

Julia does three passes through the type, two where it calls cconstruct(x::TypeVar, y) and then one where it passes in the given type. At that point, the function can do calculations on the incoming types.

If the code can vary the container type parametrically, then it helps to abstract across the container types the exact interface this code requires. This will look something like the Boost property_map interface. There are two containers in this class, one for vertices and one for neighbors of a vertex, and they are used slightly differently. The vertex_descriptor will always be some sort of key, whether it is a reference to an object in a Deque or an Int pointing into a Vector. On the other hand, the edge_descriptor in Boost.Graph isn’t just a pair of vertices. It’s a pair of (vertex_descriptor, edge_pointer). In Julia, that becomes (vertex_descriptor, edge instance), no matter whether neighbor edges are stored in a Set, Deque, or Vector.

To this end, there are a set of methods defined for each type of container. Here are two.:

container_key{V}(::Type{Vector{V}})=Int
container_value{V}(::Type{Vector{V}})=V
container_construct{V}(::Type{Vector{V}})=Array(V, 0)
container_push{V}(c::Vector{V}, v::V)=begin push!(c, v); length(c) end
container_add{V}(c::Vector{V}, args...)=begin v=V(args...); push!(c, v); v end
container_add_key{V}(c::Vector{V}, args...)=begin push!(c, V(args...)); length(c) end
container_get{V}(c::Vector{V}, k::Int)=c[k]
container_set{V}(c::Vector{V}, k::Int, v::V)=begin c[k]=v; k end
container_iter{V}(c::Vector{V})=1:length(c)
container_value_iter{V}(c::Vector{V})=c

container_key{V}(::Type{Deque{V}})=V
container_value{V}(::Type{Deque{V}})=V
container_construct{V}(::Type{Deque{V}})=Deque{V}()
container_push{V}(c::Deque{V}, v::V)=begin push!(c, v); v end
container_add{V}(c::Deque{V}, args...)=begin v=V(args...); push!(c, v); v end
container_add_key{V}(c::Deque{V}, args...)=container_add(c, args...)
container_get{V}(c::Deque{V}, k::V)=k
container_set{V}(c::Deque{V}, k::V, v::V)=begin c[k]=v; v end
container_iter{V}(c::Deque{V})=c
container_value_iter{V}(c::Deque{V})=c

These abstract the container type, detecting the type of the container that was created.

Lastly, Boost.Graph has a special integer vertex_descriptor when both containers are vectors, but the integer descriptor is possible when only the vertex container is a vector. As well, it is possible to use a Dict to store vertices (which would also be possible in Boost if it weren’t already so complicated), so this container is included in the code, too.

Table Of Contents

Previous topic

Introduction

This Page