Real-time queue in Python
A persistent queue with worst-case O(1) operations obtained by incrementally reversing the front list.
- File
real_time_queue.py, 164 lines- Level
- I0 not checked yet
- Toolchain
- Python 3.14, pytest 9
- Test framework
- pytest
- Run the tests
python3 -m pytest
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/real-time-queue/. The authors are listed in prov/X9W66J.jsonl.
"""Okasaki's real-time queue: a persistent FIFO queue with O(1) worst case.
The queue is a lazy front stream f, a rear list r and a schedule s, which
is a suffix of f that has not been evaluated yet. Invariant: |s| = |f| - |r|.
Each push and pop forces one cell of s, so the rotation f ++ reverse(r) is
evaluated one step at a time, and no operation does more than O(1) work.
Memoized suspensions make this hold even when an old version is reused.
"""
from typing import Callable, Generic, Iterator, Optional, Tuple, TypeVar
T = TypeVar("T")
# Number of suspended steps evaluated so far, for tests of the O(1) bound.
STEPS = 0
class _Stream(Generic[T]):
"""A memoized lazy list: forcing gives None (empty) or (head, tail)."""
__slots__ = ("_thunk", "_cell", "_done")
def __init__(self, thunk: "Optional[Callable[[], Optional[Tuple[T, _Stream[T]]]]]" = None) -> None:
self._thunk = thunk
self._cell: "Optional[Tuple[T, _Stream[T]]]" = None
self._done = thunk is None
def force(self) -> "Optional[Tuple[T, _Stream[T]]]":
if not self._done:
global STEPS
STEPS += 1
assert self._thunk is not None
self._cell = self._thunk()
self._thunk = None
self._done = True
return self._cell
def is_forced(self) -> bool:
return self._done
_EMPTY: "_Stream[object]" = _Stream()
class _Cons(Generic[T]):
"""An immutable cell of the rear list."""
__slots__ = ("head", "next")
def __init__(self, head: T, next: "Optional[_Cons[T]]") -> None:
self.head = head
self.next = next
def _evaluated(head: T, tail: "_Stream[T]") -> "_Stream[T]":
s: _Stream[T] = _Stream()
s._cell = (head, tail)
return s
def _rotate(f: "_Stream[T]", r: "_Cons[T]", a: "_Stream[T]") -> "_Stream[T]":
"""The stream f ++ reverse(r) ++ a, built lazily; needs |r| = |f| + 1."""
def step() -> "Optional[Tuple[T, _Stream[T]]]":
cell = f.force() # @line L1
if cell is None:
return (r.head, a)
assert r.next is not None
return (cell[0], _rotate(cell[1], r.next, _evaluated(r.head, a)))
return _Stream(step)
class RealTimeQueue(Generic[T]):
"""An immutable FIFO queue. Every operation is O(1) in the worst case."""
__slots__ = ("_f", "_r", "_s", "_lenf", "_lenr", "_lens")
def __init__(self) -> None:
"""The empty queue. Cost: O(1)."""
self._f: _Stream[T] = _EMPTY # type: ignore[assignment]
self._r: Optional[_Cons[T]] = None
self._s: _Stream[T] = _EMPTY # type: ignore[assignment]
self._lenf = 0
self._lenr = 0
self._lens = 0
@staticmethod
def _exec(
f: "_Stream[T]", lenf: int, r: "Optional[_Cons[T]]", lenr: int, s: "_Stream[T]", lens: int
) -> "RealTimeQueue[T]":
q: RealTimeQueue[T] = RealTimeQueue.__new__(RealTimeQueue)
cell = s.force()
if cell is not None: # @line L2
q._f, q._r, q._s = f, r, cell[1]
q._lenf, q._lenr, q._lens = lenf, lenr, lens - 1
return q
assert r is not None
f2 = _rotate(f, r, _EMPTY) # type: ignore[arg-type] # @line L3
q._f, q._r, q._s = f2, None, f2
q._lenf, q._lenr, q._lens = lenf + lenr, 0, lenf + lenr
return q
def __len__(self) -> int:
"""Number of items. Cost: O(1)."""
return self._lenf + self._lenr
def is_empty(self) -> bool:
"""Whether the queue is empty. Cost: O(1)."""
return self._lenf == 0
def push(self, x: T) -> "RealTimeQueue[T]":
"""The queue with x added at the back. Cost: O(1) worst case."""
return RealTimeQueue._exec( # @line L4
self._f, self._lenf, _Cons(x, self._r), self._lenr + 1, self._s, self._lens
)
def front(self) -> T:
"""The item at the front. Cost: O(1) worst case."""
cell = self._f.force()
if cell is None: # @line L5
raise IndexError("front of empty queue")
return cell[0]
def pop(self) -> "Tuple[T, RealTimeQueue[T]]":
"""The front item and the queue without it. Cost: O(1) worst case."""
cell = self._f.force()
if cell is None: # @line L6
raise IndexError("pop from empty queue")
rest = RealTimeQueue._exec( # @line L7
cell[1], self._lenf - 1, self._r, self._lenr, self._s, self._lens
)
return cell[0], rest
def __iter__(self) -> Iterator[T]:
"""Items from front to back, by repeated pop. Cost: Θ(n)."""
q = self
while not q.is_empty():
x, q = q.pop()
yield x
def check_invariant(self) -> None:
"""Check |s| = |f| - |r|, that s is the unevaluated suffix of f, and |r|.
The first |f| - |s| cells of f must be evaluated, and the stream after
them must be s itself. Walks only evaluated cells. Cost: Θ(n).
"""
assert self._lens == self._lenf - self._lenr >= 0
n = 0
c = self._r
while c is not None:
n += 1
c = c.next
assert n == self._lenr
st = self._f
for _ in range(self._lenf - self._lens):
assert st.is_forced()
cell = st.force()
assert cell is not None
st = cell[1]
assert st is self._s
test_real_time_queue.py 102 lines
# SPDX-License-Identifier: MIT
# Part of https://algorithm.tamnd.com/real-time-queue/. The authors are listed in prov/X9W66J.jsonl.
import random
import pytest
import real_time_queue as rtq
from real_time_queue import RealTimeQueue
def _state(q): # type: ignore[no-untyped-def]
return (q._lenf, q._lenr, q._lens)
def test_example() -> None:
q0: RealTimeQueue[int] = RealTimeQueue()
q1 = q0.push(1)
assert _state(q1) == (1, 0, 1)
q2 = q1.push(2)
assert _state(q2) == (1, 1, 0)
q3 = q2.push(3)
assert _state(q3) == (3, 0, 3)
q4 = q3.push(4)
assert _state(q4) == (3, 1, 2)
q5 = q4.push(5)
assert _state(q5) == (3, 2, 1)
x, q6 = q5.pop()
assert x == 1 and _state(q6) == (2, 2, 0)
q7 = q6.push(6)
assert _state(q7) == (5, 0, 5)
assert list(q7) == [2, 3, 4, 5, 6]
assert list(q5) == [1, 2, 3, 4, 5]
assert list(q2) == [1, 2]
for q in (q0, q1, q2, q3, q4, q5, q6, q7):
q.check_invariant()
def test_edges() -> None:
e: RealTimeQueue[str] = RealTimeQueue()
assert e.is_empty() and len(e) == 0 and list(e) == []
with pytest.raises(IndexError):
e.pop()
with pytest.raises(IndexError):
e.front()
one = e.push("a")
assert one.front() == "a" and len(one) == 1
x, back = one.pop()
assert x == "a" and back.is_empty() and not one.is_empty()
back.check_invariant()
def test_deep_and_worst_case_steps() -> None:
q: RealTimeQueue[int] = RealTimeQueue()
worst = 0
for i in range(100000):
before = rtq.STEPS
q = q.push(i)
worst = max(worst, rtq.STEPS - before)
assert len(q) == 100000
snapshot = q
out = []
for _ in range(50000):
before = rtq.STEPS
x, q = q.pop()
worst = max(worst, rtq.STEPS - before)
out.append(x)
assert out == list(range(50000))
# Reuse an old version many times: each pop is still O(1).
for _ in range(1000):
before = rtq.STEPS
x, _ = snapshot.pop()
worst = max(worst, rtq.STEPS - before)
assert x == 0
assert worst <= 3
q.check_invariant()
snapshot.check_invariant()
def test_random_version_tree() -> None:
rng = random.Random(1999)
for _ in range(500):
versions = [RealTimeQueue()]
model: "list[list[int]]" = [[]]
for _ in range(rng.randint(1, 60)):
v = rng.randrange(len(versions))
if rng.random() < 0.6 or not model[v]:
x = rng.randint(0, 99)
versions.append(versions[v].push(x))
model.append(model[v] + [x])
else:
x, q = versions[v].pop()
assert x == model[v][0]
versions.append(q)
model.append(model[v][1:])
versions[-1].check_invariant()
for q, m in zip(versions, model):
assert len(q) == len(m)
assert list(q) == m
if m:
assert q.front() == m[0]
q.check_invariant()
What the next level needs
- the tests did not run in this build