Linked Lists
Radish implements a custom doubly-linked list rather than using Julia’s built-in Vector. This is a deliberate design choice. The goal was to learn how to build custom structures rather than using built-in types. In addition to that, the goal was to obtain a classic double-linked list implementation with fast performances for head/tail operations rather than random access.
Why Not Use Arrays?
Simple comparison between Vector type in Julia and a Doubly-Linked List type.
| Operation | Array (Vector) | Doubly-Linked List |
|---|---|---|
| Push to tail | O(1) amortized | O(1) |
| Push to head | O(n) — shifts all elements | O(1) |
| Pop from tail | O(1) | O(1) |
| Pop from head | O(n) — shifts all elements | O(1) |
| Random access | O(1) | O(n) |
| Memory overhead | Lower | Higher (prev/next pointers) |
Redis uses linked lists (or more precisely, quicklists) for its List type because the primary use case is queue/stack operations — push and pop from either end. Random access (LRANGE) is less common and can tolerate O(n).
Radish follows the same reasoning: L_PREPEND, L_APPEND, L_POP, and L_DEQUEUE are all O(1).
The DLinkedStartEnd Structure
mutable struct DLinkedNode
value::String
prev::Union{DLinkedNode, Nothing}
next::Union{DLinkedNode, Nothing}
end
mutable struct DLinkedStartEnd
start::Union{DLinkedNode, Nothing} # Head pointer
finish::Union{DLinkedNode, Nothing} # Tail pointer
len::Int # Cached length
end
The structure maintains pointers to both the head (start) and tail (finish), plus a cached length so that L_LEN is O(1) without traversal.
Basic Operations
RADISH-CLI> L_ADD tasks "first task" # Create a list
OK
RADISH-CLI> L_APPEND tasks "second task" # Add to tail
OK
RADISH-CLI> L_PREPEND tasks "urgent task" # Add to head
OK
RADISH-CLI> L_GET tasks # View all
✅ [urgent task, first task, second task]
RADISH-CLI> L_LEN tasks
✅ 3
Queue / Stack Patterns
The list supports both queue (FIFO) and stack (LIFO) patterns:
# Queue (FIFO): push to tail, dequeue from head
RADISH-CLI> L_APPEND queue "job1"
RADISH-CLI> L_APPEND queue "job2"
RADISH-CLI> L_DEQUEUE queue # → job1 (first in, first out)
# Stack (LIFO): push to tail, pop from tail
RADISH-CLI> L_APPEND stack "frame1"
RADISH-CLI> L_APPEND stack "frame2"
RADISH-CLI> L_POP stack # → frame2 (last in, first out)
List Merging
L_MOVE is a powerful operation that moves one list to the end of another, consuming the source list:
RADISH-CLI> L_MOVE list1 list2
This is an O(1) operation — it just relinks the tail of list1 to the head of list2. The key for list2 is deleted after the operation.
Trimming
Lists can be trimmed from either end:
RADISH-CLI> L_TRIMR tasks 2 # Keep only first 2 elements
RADISH-CLI> L_TRIML tasks 1 # Keep only last 1 element
Auto-Cleanup
When a list becomes empty (e.g., after popping the last element), Radish automatically deletes the key from the context. This prevents “ghost keys” — empty lists taking up space in the dictionary. Redis does the same thing.