Storage Patterns: Stacks Queues and Deques

Stack

A stack is a “last-in, first-out” (LIFO) data structure with the operations push and pop. This is a natural fit for Solidity’s dynamically-sized arrays, but note that Solidity does not (yet) provide a pop operation. The below is an implementation of a stack of bytes:

If the array is empty, bounds checking will cause pop to revert the transaction.

Decreasing the length of a dynamically-sized array has a side effect of zeroing the “removed” items. This is why there’s no need for a delete stack[stack.length - 1].

Queue

A queue is a “first-in, first-out” (FIFO) data structure with the operations enqueue and dequeue. It’s reasonable to implement this with a dynamically-sized array, but a mapping may be a better choice:

Items are stored at consecutive indices in the queue mapping. The first used index will be 1.

first indicates the first valid index in the queue. It starts at 1 so I can use 0 for last.

last indicates the last valid index in the queue. It starts at 0 because there are initially no valid indices.

Unlike in the stack contract, where decreasing the array size took care of it, this contract needs to call delete to clear storage when an element is dequeued.

Dynamically-sized arrays and mappings in Solidity are not very different. They both map keys to values. Arrays are limited to uint256 keys, and they do bounds checking. Dynamically-sized arrays also record their length and perform some automated cleanup when that length is adjusted.

In the stack implementation, I made use of the bounds checking and automatic cleanup. In the case of a queue, both of those things need to be done manually, at least on the “left” side of the queue. For that reason, a mapping seemed like a more natural fit to me.

Deque

A deque (sometimes “dequeue” or “double-ended queue”) is a generalization of a queue that allows insertion and deletion from both ends. It supports the operations pushLeft, pushRight, popLeft, and popRight:

This code is much like the queue implementation except that the queue can grow in both directions. To maximize how much room is available to grow, first is initialized to 2255, exactly half way through the 256-bit space available for indices.

Summary

With a basic understanding of Solidity storage, simple data structures can be implemented with very little code.

Dynamically-sized arrays and mappings are quite similar. In many cases, either can be used, and the decision comes down to what makes the code easier to read.