View Source Qu (qu v0.1.0)

Qu is an Elixir library that provides a single interface to four different types of queues: FIFO, LIFO, circular, and priority.

Usage

All of the queue types support the same operations: Qu.put/2 to add an item, Qu.pop/1 to remove the head, Qu.peek/1 to view the head without removal, Qu.pull/2 to remove multiple items, and Qu.size/1 to get the number of items in the queue. These operations are demonstrated below. For more details about the queue operations, see the documentation for each function.

To create and fill a FIFO queue with a maximum size of 3, use Qu.new/2 and then apply Qu.put/2 in a reduction:

fifo = Qu.new(:fifo, max_size: 3) 
#=> #FIFO<read: [], write: [], size: 0, max_size: 3>

fifo =
  Enum.reduce(["a", "b", "c"], fifo, fn item, q ->
    {:ok, q} = Qu.put(q, item)
    q
  end)
#=> #FIFO<read: [], write: ["c", "b", "a"], size: 3, max_size: 3>

A priority queue expects items of the form {priority_key, value}. Here is how to create and populate a priority queue where items are popped in ascending order of priority:

prio = Qu.new(:priority, priority_order: :asc)

Enum.reduce([{1, "a"}, {2, "b"}, {3, "c"}], prio, fn item, q ->
  {:ok, q} = Qu.put(q, item)
  q
end)
#=> #Priority<heap: #PairingHeap<root: {1, "a"}, size: 3, mode: :min>, size: 3, max_size: nil>

For FIFO, LIFO, and priority queues, applying Qu.put/2 to a full queue returns :error:

Qu.new(:fifo, max_size: 0) |> Qu.put(fifo, "a")
#=> :error

An error is never returned when adding items to a circular queue, since the oldest item is discarded when inserting into a full circular queue. To preserve the common interface, Qu.put/2 still returns {:ok, updated_queue} for a circular queue.

The head of the queue can be removed and returned with Qu.pop/1:

{:ok, fifo} = Qu.new(:fifo) |> Qu.put("a")
Qu.pop(fifo)
#=> {:ok, "a", #FIFO<read: [], write: [], size: 0, max_size: nil>}

For all queue types, Qu.pop/1 returns :error if the queue is empty:

Qu.new(:fifo, max_size: 0) |> Qu.pop()
#=> :error

The head can be viewed without modifying the queue using Qu.peek/1:

{:ok, fifo} = Qu.new(:fifo) |> Qu.put("a")
Qu.peek(fifo)
#=> {:ok, "a"}

Similar to Qu.pop/1, Qu.peek/1 will return :error if the queue is empty.

Use Qu.pull/2 to remove and return multiple items:

fifo = Qu.new(:fifo, max_size: 3)

["a", "b", "c"]
|> Enum.reduce(fifo, fn item, q ->
  {:ok, q} = Qu.put(q, item)
  q
end)
|> Qu.pull(3)
#=> {["a", "b", "c"], #FIFO<read: [], write: [], size: 0, max_size: 3>}

Note that Qu.pull/2 does not return :error if the queue is empty or if the number of elements pulled is larger than the queue size.

For all of the queue types, Qu.size/1 returns the current size of the queue:

{:ok, fifo} = Qu.new(:fifo) |> Qu.put("a")
Qu.size(fifo)
#=> 1

All of the queue types also implement the Enumerable and Collectable protocols, so that Enum.reduce/3, Enum.to_list/1, and Enum.into/2 may be used.

Supported queue types

Listed below are the supported queue types, as well as notes about their implementations and usage guidelines.

FIFO

A first-in, first-out queue, in which the oldest item in the queue will be the next item removed. If the queue has a finite maximum size and is full, the next insert will fail.

The implementation follows the Erlang queue and uses separate read and write lists to achieve O(1) amortized time for insert and delete operations.

LIFO

A last-in, first-out queue, in which the item most recently added to the queue will be the next item removed. This is also known as a stack. If the queue has a finite maximum size and is full, the next insert will fail.

The implementation uses a single Elixir list and has O(1) time for both insert and delete operations.

Circular

A circular queue is similar to a FIFO queue in that the oldest item in the queue will be the next item removed. However, when adding an item to a full circular queue, the oldest item will be discarded to preserve the fixed size. An insert in a full circular queue will always succeed.

The implementation borrows from the FIFO implementation, but has a modified pop/1 function to achieve the properties described above.

Priority

In a priority queue, items are added as key-value pairs, where the key is a sortable priority. Items are removed in priority order, either descending or ascending, depending on the configuration.

The implementation uses a heap, specifically a pairing heap (see the PairingHeap Elixir library), which has O(1) insert time and O(log n) amortized deletion time.

Summary

Types

A queue item, either a single value or, in the case of a priority queue, a tuple of a priority key and a value.

Maximum queue size. A value of nil indicates that the queue has no maximum size.

Type of queue.

Current size of the queue.

Functions

Create a new queue of the given type and maximum size.

Get the item at the head of the queue.

Pop the item at the head of the queue.

Return the first n items in the queue and the final state of the heap.

Insert an item into the queue.

Return the size of the queue.

Types

@type item() :: any() | {any(), any()}

A queue item, either a single value or, in the case of a priority queue, a tuple of a priority key and a value.

@type max_size() :: non_neg_integer() | nil

Maximum queue size. A value of nil indicates that the queue has no maximum size.

@type queue_type() :: :fifo | :lifo | :circular | :priority

Type of queue.

@type size() :: non_neg_integer()

Current size of the queue.

Functions

@spec new(type :: queue_type(), opts :: keyword()) :: Qu.Queue.t()

Create a new queue of the given type and maximum size.

Options

  • :max_size - Maximum size of the queue. A value of nil indicates that the queue has no maximum size. When the queue type is :circular, the maximum must be a positive integer. The default value is nil.

  • :priority_order - Key order in which items are removed when the queue type is :priority. When a tuple like {:desc, DateTime} is given, the key is assumed to be a DateTime, and DateTime.compare/2 is used compare keys. The default value is :asc.

Examples

iex> Qu.new(:fifo, max_size: 10)
#FIFO<read: [], write: [], size: 0, max_size: 10>

iex> Qu.new(:priority, priority_order: {:asc, DateTime})
#Priority<heap: #PairingHeap<root: :empty, size: 0, mode: {:min, DateTime}>, size: 0, max_size: nil>
@spec peek(Qu.Queue.t()) :: {:ok, item()} | :error

Get the item at the head of the queue.

If there is at least one item in the queue, this returns {:ok, item}. If the queue is empty, :error is returned.

Examples

iex> {:ok, q} = Qu.new(:fifo, max_size: 1) |> Qu.put("a")
iex> {:ok, "a"} = Qu.peek(q)
@spec pop(Qu.Queue.t()) :: {:ok, item(), Qu.Queue.t()} | :error

Pop the item at the head of the queue.

If there is at least one item in the queue, this returns {:ok, item, updated_queue}. If the queue is empty, :error is returned.

Examples

iex> {:ok, q} = Qu.new(:fifo) |> Qu.put("a")
iex> {:ok, "a", q} = Qu.pop(q)
iex> q
#FIFO<read: [], write: [], size: 0, max_size: nil>
iex> Qu.pop(q)
:error
@spec pull(Qu.Queue.t(), non_neg_integer()) :: {[item()], Qu.Queue.t()}

Return the first n items in the queue and the final state of the heap.

If the queue size is less than n, all items are returned along with an empty heap.

Note that pull is often more convenient than pop, because of it can pop multiple items at once, and because it never returns :error.

Examples

iex> {:ok, q} = Qu.new(:fifo, max_size: 1) |> Qu.put("a")
iex> {["a"], _} = Qu.pull(q, 1)
@spec put(Qu.Queue.t(), item()) :: {:ok, Qu.Queue.t()} | :error

Insert an item into the queue.

If the queue size is less than the maximum size, this returns {:ok, updated_queue}. With the exception of a cirular queue, if the queue size equals the maximum size, :error is returned. For a circular queue, Qu.put/2 never returns :error.

Examples

iex> {:ok, q} = Qu.new(:fifo, max_size: 1) |> Qu.put("a")
iex> q
#FIFO<read: [], write: ["a"], size: 1, max_size: 1>
iex> Qu.put(q, "b")
:error

iex> {:ok, q}  = Qu.new(:priority, priority_order: :asc) |> Qu.put({1, "a"})
iex> q
#Priority<heap: #PairingHeap<root: {1, "a"}, size: 1, mode: :min>, size: 1, max_size: nil>
@spec size(Qu.Queue.t()) :: size()

Return the size of the queue.

Examples

iex> q = Qu.new(:fifo, max_size: 1)
iex> Qu.size(q)
0
iex> {:ok, q} = Qu.put(q, "a")
iex> Qu.size(q)
1