dream_ets
Type-safe ETS (Erlang Term Storage) for Gleam.
A standalone module providing a type-safe interface to Erlang’s ETS in-memory storage. Features a builder pattern for table configuration, type-safe operations, and comprehensive error handling. Built with the same quality standards as Dream, but completely independent—use it in any Gleam project.
Features
- ✅ Type-safe - Keys and values are typed at compile time
- ✅ Result-based errors - All operations return
Resultfor explicit error handling - ✅ Builder pattern - Composable, fluent table configuration
- ✅ Atomic operations -
insert_new(),take()for race-free operations - ✅ Custom types - Store any type with custom encoders (JSON, tuples, etc.)
- ✅ Pattern matching - Advanced ETS queries and match specifications
- ✅ Table persistence - Save/load tables to disk
- ✅ 100% tested - Comprehensive test coverage with verified examples
- ✅ Zero dependencies - No Dream or framework requirements
Installation
gleam add dream_ets
Quick Start
Creating a Table
import dream_ets/config
import dream_ets/operations
import gleam/option
import gleam/result
pub fn create_string_table() -> Result(String, table.EtsError) {
// Create a table using the builder pattern
use cache <- result.try(
config.new("user_cache")
|> config.key_string()
|> config.value_string()
|> config.create(),
)
// Use it
use _ <- result.try(operations.set(cache, "alice", "Alice"))
use value <- result.try(operations.get(cache, "alice"))
case value {
option.Some(name) -> Ok(name)
option.None -> Error(table.OperationFailed("Not found"))
}
}
Basic Operations
import dream_ets/helpers
import dream_ets/operations
import gleam/option
import gleam/result
pub fn store_and_retrieve() -> Result(String, table.EtsError) {
use cache <- result.try(helpers.new_string_table("cache"))
// Store a value
use _ <- result.try(operations.set(cache, "greeting", "Hello, World!"))
// Retrieve it
use value <- result.try(operations.get(cache, "greeting"))
case value {
option.Some(greeting) -> Ok(greeting)
option.None -> Error(table.OperationFailed("Not found"))
}
}
Counter Tables
import dream_ets/helpers
import gleam/result
pub fn increment_page_views() -> Result(Int, table.EtsError) {
use counter <- result.try(helpers.new_counter("page_views"))
// Track multiple page views
use _ <- result.try(helpers.increment(counter, "homepage"))
use _ <- result.try(helpers.increment(counter, "homepage"))
use count <- result.try(helpers.increment(counter, "homepage"))
Ok(count) // Returns 3
}
Core Features
Custom Types with JSON
Store your own types using JSON encoding:
import dream_ets/config
import dream_ets/internal
import dream_ets/operations
import gleam/dynamic
import gleam/dynamic/decode
import gleam/json
import gleam/option
import gleam/result
pub type User {
User(name: String, email: String)
}
fn encode_user(user: User) -> dynamic.Dynamic {
json.object([
#("name", json.string(user.name)),
#("email", json.string(user.email)),
])
|> json.to_string
|> internal.to_dynamic
}
fn decode_user() -> decode.Decoder(User) {
decode.string
|> decode.then(fn(json_str) {
case json.parse(json_str, user_from_json()) {
Ok(user) -> decode.success(user)
Error(_) -> decode.failure(User("", ""), "User")
}
})
}
fn user_from_json() -> decode.Decoder(User) {
use name <- decode.field("name", decode.string)
use email <- decode.field("email", decode.string)
decode.success(User(name: name, email: email))
}
pub fn store_custom_type() -> Result(String, table.EtsError) {
use users <- result.try(
config.new("users")
|> config.key_string()
|> config.value(encode_user, decode_user())
|> config.create(),
)
let user = User(name: "Alice", email: "alice@example.com")
use _ <- result.try(operations.set(users, "alice", user))
use retrieved <- result.try(operations.get(users, "alice"))
case retrieved {
option.Some(u) -> Ok(u.name <> " <" <> u.email <> ">")
option.None -> Error(table.OperationFailed("User not found"))
}
}
Preventing Duplicates
Use insert_new() for atomic “check and insert” operations:
import dream_ets/helpers
import dream_ets/operations
import gleam/result
pub fn register_user() -> Result(Bool, table.EtsError) {
use registrations <- result.try(helpers.new_string_table("registrations"))
// Try to register username
use registered <- result.try(operations.insert_new(
registrations,
"alice",
"alice@example.com",
))
case registered {
True -> Ok(True) // Username available
False -> Ok(False) // Username already taken
}
}
Atomic Operations
import dream_ets/helpers
import dream_ets/operations
import gleam/option
import gleam/result
pub fn atomic_take() -> Result(String, table.EtsError) {
use queue <- result.try(helpers.new_string_table("jobs"))
// Add a job
use _ <- result.try(operations.set(queue, "job:123", "send_email"))
// Take and remove atomically (no race conditions)
use job <- result.try(operations.take(queue, "job:123"))
case job {
option.Some(task) -> Ok(task)
option.None -> Error(table.OperationFailed("Job not found"))
}
}
Table Configuration
Optimize tables for your workload:
import dream_ets/config
import dream_ets/operations
import gleam/option
import gleam/result
pub fn configure_table() -> Result(String, table.EtsError) {
// Create table with read concurrency enabled
// Use this when multiple processes will read simultaneously
use cache <- result.try(
config.new("cache")
|> config.read_concurrency(True)
|> config.key_string()
|> config.value_string()
|> config.create(),
)
use _ <- result.try(operations.set(cache, "key", "value"))
use value <- result.try(operations.get(cache, "key"))
case value {
option.Some(v) -> Ok(v)
option.None -> Error(table.OperationFailed("Not found"))
}
}
Configuration Options:
read_concurrency(Bool)- Optimize for concurrent readswrite_concurrency(Bool)- Optimize for concurrent writescompressed(Bool)- Compress data to save memorytable_type(Set | OrderedSet | Bag | DuplicateBag)- Key handlingaccess(Public | Protected | Private)- Process access control
Type Safety
import dream_ets/helpers
import dream_ets/operations
import gleam/option
import gleam/result
pub fn type_safe_storage() -> Result(String, table.EtsError) {
// String table enforces types at compile time
use cache <- result.try(helpers.new_string_table("cache"))
// ✅ This works
use _ <- result.try(operations.set(cache, "key", "value"))
// ❌ This would be a compile error:
// operations.set(cache, 123, "value")
// Error: Expected String, found Int
use value <- result.try(operations.get(cache, "key"))
case value {
option.Some(v) -> Ok(v)
option.None -> Error(table.OperationFailed("Not found"))
}
}
Persistence
import dream_ets/helpers
import dream_ets/operations
import gleam/option
import gleam/result
pub fn save_to_disk() -> Result(String, table.EtsError) {
use table <- result.try(helpers.new_string_table("data"))
use _ <- result.try(operations.set(table, "key", "important data"))
// Save to disk
use _ <- result.try(operations.save_to_file(table, "/tmp/backup.ets"))
// Verify it's still there
use value <- result.try(operations.get(table, "key"))
case value {
option.Some(data) -> Ok(data)
option.None -> Error(table.OperationFailed("Data lost"))
}
}
Complete API Reference
Table Creation
config.new(name)- Create table configurationconfig.key_string(config)- Set string keysconfig.value_string(config)- Set string valuesconfig.key(config, encoder, decoder)- Set custom key encodingconfig.value(config, encoder, decoder)- Set custom value encodingconfig.counter(config)- Configure counter table (String keys, Int values)config.create(config)- Create table from configurationhelpers.new_counter(name)- Convenience: create counter tablehelpers.new_string_table(name)- Convenience: create string table
Basic Operations
operations.set(table, key, value)- Insert or update valueoperations.get(table, key)- Retrieve value (returnsOption)operations.delete(table, key)- Remove key-value pairoperations.member(table, key)- Check if key exists (fast)operations.delete_table(table)- Delete entire tableoperations.delete_all_objects(table)- Clear all entries
Atomic Operations
operations.insert_new(table, key, value)- Insert only if key doesn’t exist (atomic)operations.take(table, key)- Get and remove atomically (for queues)
Bulk Operations
Note: These now return Result to handle decode errors properly:
operations.keys(table)- Get all keys →Result(List(k), EtsError)operations.values(table)- Get all values →Result(List(v), EtsError)operations.to_list(table)- Get all pairs →Result(List(#(k, v)), EtsError)operations.size(table)- Count entries →Result(Int, EtsError)
Counter Operations
helpers.increment(counter, key)- Increment by 1helpers.increment_by(counter, key, amount)- Increment by amounthelpers.decrement(counter, key)- Decrement by 1helpers.decrement_by(counter, key, amount)- Decrement by amount
Advanced (Low-level)
operations.update_element(table, key, pos, value)- Update tuple elementoperations.match(table, pattern)- Pattern matchingoperations.match_object(table, pattern)- Object matchingoperations.select(table, match_spec)- SQL-like queries
Persistence
operations.save_to_file(table, filename)- Save table to diskoperations.load_from_file(filename)- Load table from disk
Error Handling
All operations return Result types. Common errors:
TableNotFound- Table was deletedTableAlreadyExists- Tried to create duplicate tableInvalidKey/InvalidValue- Encoding/decoding failedDecodeError(details)- Failed to decode data (corruption or encoder mismatch)OperationFailed(message)- General operation failure
Example:
case operations.get(table, "user:123") {
Ok(option.Some(user)) -> process_user(user)
Ok(option.None) -> create_user()
Error(table.DecodeError(err)) -> log_corruption(err)
Error(table.TableNotFound) -> recreate_table()
Error(other) -> handle_error(other)
}
Use Cases
Session Cache
use sessions <- result.try(
config.new("sessions")
|> config.key_string()
|> config.value_string()
|> config.create(),
)
use _ <- result.try(operations.set(sessions, "session:abc", "user:alice"))
Page View Analytics
use counter <- result.try(helpers.new_counter("analytics"))
use views <- result.try(helpers.increment(counter, "homepage"))
Distributed Locks
case operations.insert_new(locks, resource_id, owner_id) {
Ok(True) -> Ok("Lock acquired")
Ok(False) -> Error("Resource already locked")
Error(err) -> Error("Lock system error")
}
Work Queue
case operations.take(queue, "next_job") {
Ok(option.Some(job)) -> process_job(job) // Atomically claimed
Ok(option.None) -> wait_for_jobs()
Error(err) -> handle_error(err)
}
Performance Notes
- Basic ops (
set,get,delete,member) - O(1) constant time - Bulk ops (
keys,values,to_list,size) - O(n) iterates entire table - Concurrency - Enable
read_concurrencyfor read-heavy workloads - Compression - Enable for large values to trade CPU for memory
Design Principles
This module follows the same quality standards as Dream:
- Explicit over implicit - No hidden behavior, no magic
- Result-based errors - All operations return
Resultto force error handling - No closures - All dependencies are explicit parameters
- Simple over clever - Code should be obvious and boring
- Type-safe - Leverage Gleam’s type system fully
- Black-box testing - Test public interfaces, 100% coverage
All Examples Are Tested
Every code example in this README comes from test/snippets/ and is verified by our test suite. You can run them yourself:
cd modules/ets
gleam test
See test/snippets/ for complete, runnable examples.
About Dream
This module was originally built for the Dream web toolkit, but it’s completely standalone and can be used in any Gleam project. It follows Dream’s design principles and will be maintained as part of the Dream ecosystem.
License
MIT License - see LICENSE file for details.