dream_ets/config
Table configuration and builder pattern
This module provides the builder pattern for creating ETS tables with type-safe configuration. Use this module to create tables with custom settings for concurrency, access control, and data types.
Quick Start
import dream_ets/config
// Create a simple string-to-string table
let assert Ok(table) = config.new("users")
|> config.key_string()
|> config.value_string()
|> config.create()
// Create a counter table (String keys, Int values)
let assert Ok(counter) = config.new("page_views")
|> config.counter()
|> config.create()
// Create with custom configuration
let assert Ok(table) = config.new("cache")
|> config.table_type(config.table_type_set())
|> config.access(config.access_public())
|> config.read_concurrency(True)
|> config.write_concurrency(False)
|> config.key_string()
|> config.value_string()
|> config.create()
Builder Pattern
The configuration follows the builder pattern:
- Start with
new(name)to create a config - Chain configuration methods to customize settings
- Specify key and value encoders (required)
- Call
create()to build the table
Type Safety
Tables are parameterized by key and value types. The type system ensures you cannot insert wrong types into a table.
Types
Access control for ETS tables
Determines which processes can read and write to the table.
Variants
Public: Any process can read and write. Recommended for most cases.Protected: Owner can write, any process can read. Use for read-heavy workloads.Private: Only the owner process can read and write. Rarely needed.
Note: In Dream applications, Public is usually the right choice since
the table typically outlives individual request processes.
pub type Access {
Public
Protected
Private
}
Constructors
-
Public -
Protected -
Private
Table configuration builder
Opaque type that holds all configuration for creating an ETS table.
Use the builder methods to configure the table before calling create().
Type Parameters
key: The type of keys that will be stored in the tablevalue: The type of values that will be stored in the table
pub opaque type TableConfig(key, value)
Table type for ETS tables
Determines how the table handles duplicate keys and ordering.
Variants
Set: Keys are unique, no ordering guarantees. Default and recommended.OrderedSet: Keys are unique and sorted. Slower inserts but ordered iteration.Bag: Keys can have multiple values, order not preserved.DuplicateBag: Keys can have multiple identical values.
Note: Most applications should use Set (the default). Only use other
types if you have specific requirements for ordering or duplicate values.
pub type TableType {
Set
OrderedSet
Bag
DuplicateBag
}
Constructors
-
Set -
OrderedSet -
Bag -
DuplicateBag
Values
pub fn access(
config: TableConfig(k, v),
access_mode: Access,
) -> TableConfig(k, v)
Set the access mode
Controls which processes can read and write to the table. The default is
Public, which allows any process to access the table.
Parameters
config: The table configuration to modifyaccess_mode: The access mode (Public, Protected, or Private)
Returns
The updated configuration.
Example
import dream_ets/config
// Protected: only owner can write, anyone can read
let assert Ok(table) = config.new("config_cache")
|> config.access(config.access_protected())
|> config.key_string()
|> config.value_string()
|> config.create()
Access Modes
- Public: Any process can read/write (recommended)
- Protected: Owner writes, anyone reads
- Private: Only owner can read/write (rarely needed)
pub fn access_private() -> Access
Create Private access mode
Returns Private access. Only the owner process can read and write.
This is rarely needed in most applications.
Returns
A Private access mode.
pub fn access_protected() -> Access
Create Protected access mode
Returns Protected access. Only the owner process can write, but any
process can read. Use this for read-heavy workloads where you want to
control writes.
Returns
A Protected access mode.
Example
import dream_ets/config
// Useful for configuration that rarely changes
let table = config.new("config")
|> config.access(config.access_protected())
|> config.key_string()
|> config.value_string()
|> config.create()
pub fn access_public() -> Access
Create Public access mode
Returns Public access, allowing any process to read and write to the
table. This is the recommended default for most applications.
Returns
A Public access mode.
Example
import dream_ets/config
let table = config.new("sessions")
|> config.access(config.access_public())
|> config.key_string()
|> config.value_string()
|> config.create()
pub fn compressed(
config: TableConfig(k, v),
enabled: Bool,
) -> TableConfig(k, v)
Enable or disable table compression
Compresses table data to reduce memory usage. This trades CPU time for memory space. Only consider this for tables with large values.
Parameters
config: The table configuration to modifyenabled:Trueto enable compression,Falseto disable (default)
Returns
The updated configuration.
Example
import dream_ets/config
// Compress large JSON values
let assert Ok(table) = config.new("documents")
|> config.compressed(True)
|> config.key_string()
|> config.value_string()
|> config.create()
When to Use
- Enable: Storing large values (e.g., JSON documents, HTML)
- Disable: Small values or performance-critical code (default)
Trade-offs
- Pro: Reduces memory usage (especially for large, compressible data)
- Con: Slower read/write operations due to compression overhead
- Note: Only worth it if values are large and compressible
pub fn counter(
config: TableConfig(String, Int),
) -> TableConfig(String, Int)
Convenience: Configure as counter table (String keys, Int values)
Configures the table for use as a counter with string keys and integer values. This is a convenience function that sets both key and value encoders appropriately.
Parameters
config: The table configuration to modify
Returns
The updated configuration ready for use as a counter.
Example
import dream_ets/config
import dream_ets/helpers
// Create a counter table
let assert Ok(counter) = config.new("page_views")
|> config.counter()
|> config.create()
// Use with increment helpers
let assert Ok(count) = helpers.increment(counter, "homepage")
See Also
helpers.new_counter()- Even simpler counter creationhelpers.increment()- Increment counter valueshelpers.decrement()- Decrement counter values
pub fn create(
config: TableConfig(k, v),
) -> Result(table.Table(k, v), table.EtsError)
Create the table from configuration
Validates the configuration and creates the ETS table. This is the final step in the builder pattern - it actually creates the table in memory.
Parameters
config: The table configuration with all settings applied
Returns
Ok(Table): Successfully created tableError(TableAlreadyExists): A table with this name already existsError(InvalidKey): Key encoder/decoder not setError(InvalidValue): Value encoder/decoder not setError(OperationFailed): ETS creation failed
Example
import dream_ets/config
// Create a simple table
let assert Ok(table) = config.new("users")
|> config.key_string()
|> config.value_string()
|> config.create()
// Create with custom settings
let assert Ok(table) = config.new("cache")
|> config.read_concurrency(True)
|> config.write_concurrency(True)
|> config.key_string()
|> config.value_string()
|> config.create()
Errors
This function will return an error if:
- A table with the same name already exists
- Key encoder/decoder was not configured
- Value encoder/decoder was not configured
- ETS table creation fails for any reason
See Also
new()- Start the builderkey_string()/value_string()- Common encoder shortcuts
pub fn key(
config: TableConfig(k, v),
encoder: fn(k) -> dynamic.Dynamic,
decoder: decode.Decoder(k),
) -> TableConfig(k, v)
Set key encoder and decoder
Configures custom encoding/decoding functions for keys. This is required for creating a table - you must specify how to convert keys to/from Erlang’s dynamic types.
Parameters
config: The table configuration to modifyencoder: Function to convert keys toDynamicfor storagedecoder: Decoder to convertDynamicback to keys
Returns
The updated configuration.
Example
import dream_ets/config
import dream_ets/encoders
import gleam/dynamic
// Using built-in encoders
let assert Ok(table) = config.new("users")
|> config.key(encoders.string_encoder, encoders.string_decoder())
|> config.value_string()
|> config.create()
Custom Encoders
For custom types, provide your own encoder/decoder:
import gleam/dynamic/decode
pub type UserId {
UserId(Int)
}
fn encode_user_id(id: UserId) -> dynamic.Dynamic {
let UserId(n) = id
dynamic.from(n)
}
fn decode_user_id() -> decode.Decoder(UserId) {
decode.int |> decode.map(UserId)
}
let assert Ok(table) = config.new("users")
|> config.key(encode_user_id, decode_user_id())
|> config.value_string()
|> config.create()
See Also
key_string()- Convenience function for string keysvalue()- Set value encoder/decoder
pub fn key_string(
config: TableConfig(String, v),
) -> TableConfig(String, v)
Convenience: Set string key encoding
Configures the table to use string keys. This is a convenience function
that calls key() with built-in string encoders.
Parameters
config: The table configuration to modify
Returns
The updated configuration with string key encoding.
Example
import dream_ets/config
let assert Ok(table) = config.new("users")
|> config.key_string()
|> config.value_string()
|> config.create()
See Also
key()- For custom key typesvalue_string()- For string values
pub fn keypos(
config: TableConfig(k, v),
pos: Int,
) -> TableConfig(k, v)
Set the key position in tuples (default: 1)
Configures which element of a tuple to use as the key. This is an advanced feature for tuple-based tables. Most users will never need this.
Parameters
config: The table configuration to modifypos: The 1-based position of the key in tuples (default: 1)
Returns
The updated configuration.
Note
This is only relevant when storing tuples directly in ETS. For typed tables using encoders/decoders (the recommended approach), this setting is handled automatically.
pub fn new(name: String) -> TableConfig(k, v)
Create a new table configuration with sensible defaults
This is the starting point for the builder pattern. After calling new(),
you must chain methods to specify key and value encoders, then call create()
to build the table.
Defaults
- Table type:
Set- Keys are unique, fast operations - Access:
Public- Any process can read/write - Key position: 1 (for tuple-based tables)
- Read concurrency:
False- Enable withread_concurrency(True)if needed - Write concurrency:
False- Enable withwrite_concurrency(True)if needed - Compressed:
False- Can reduce memory for large values - Named table:
True- Table is registered by name
Parameters
name: A unique name for the table. Must be unique across the application.
Returns
A new TableConfig with default settings. You must set key and value
encoders before calling create().
Example
import dream_ets/config
let assert Ok(table) = config.new("users")
|> config.key_string()
|> config.value_string()
|> config.create()
See Also
create()- Create the table after configurationkey_string()- Set string keysvalue_string()- Set string values
pub fn read_concurrency(
config: TableConfig(k, v),
enabled: Bool,
) -> TableConfig(k, v)
Enable or disable read concurrency
Optimizes the table for concurrent read operations. Enable this if multiple processes will be reading from the table simultaneously.
Parameters
config: The table configuration to modifyenabled:Trueto enable read concurrency,Falseto disable (default)
Returns
The updated configuration.
Example
import dream_ets/config
// Optimize for many simultaneous readers
let assert Ok(table) = config.new("user_cache")
|> config.read_concurrency(True)
|> config.key_string()
|> config.value_string()
|> config.create()
When to Use
- Enable: High read traffic from multiple processes
- Disable: Single-threaded access or write-heavy workloads
Performance Impact
- Pro: Significantly improves concurrent read performance
- Con: Slightly increases memory usage and write overhead
pub fn table_type(
config: TableConfig(k, v),
type_: TableType,
) -> TableConfig(k, v)
Set the table type
Configures how the table handles keys and ordering. The default is Set,
which is appropriate for most use cases.
Parameters
config: The table configuration to modifytype_: The table type to use (Set, OrderedSet, Bag, or DuplicateBag)
Returns
The updated configuration.
Example
import dream_ets/config
let assert Ok(table) = config.new("sorted_cache")
|> config.table_type(config.table_type_ordered_set())
|> config.key_string()
|> config.value_string()
|> config.create()
Performance Notes
Set: Fastest for most operations (default)OrderedSet: Slower insertions but supports ordered iterationBag/DuplicateBag: Rarely needed, use only for specific use cases
pub fn table_type_bag() -> TableType
Create a Bag table type
Returns a Bag table type. A key can have multiple different values.
This is rarely needed in most applications.
Returns
A Bag table type.
pub fn table_type_duplicate_bag() -> TableType
Create a DuplicateBag table type
Returns a DuplicateBag table type. A key can have multiple identical
values. This is rarely needed in most applications.
Returns
A DuplicateBag table type.
pub fn table_type_ordered_set() -> TableType
Create an OrderedSet table type
Returns an OrderedSet table type. Keys are unique and maintained in
sorted order. Use this when you need ordered iteration, but note that
insertions are slower than Set.
Returns
An OrderedSet table type.
Example
import dream_ets/config
// Useful for leaderboards or sorted data
let table = config.new("leaderboard")
|> config.table_type(config.table_type_ordered_set())
|> config.key_string()
|> config.value_string()
|> config.create()
pub fn table_type_set() -> TableType
Create a Set table type
Returns a Set table type, which is the default and recommended type for
most use cases. Keys are unique and insertion/lookup is fast.
Returns
A Set table type.
Example
import dream_ets/config
let table = config.new("cache")
|> config.table_type(config.table_type_set())
|> config.key_string()
|> config.value_string()
|> config.create()
pub fn value(
config: TableConfig(k, v),
encoder: fn(v) -> dynamic.Dynamic,
decoder: decode.Decoder(v),
) -> TableConfig(k, v)
Set value encoder and decoder
Configures custom encoding/decoding functions for values. This is required for creating a table - you must specify how to convert values to/from Erlang’s dynamic types.
Parameters
config: The table configuration to modifyencoder: Function to convert values toDynamicfor storagedecoder: Decoder to convertDynamicback to values
Returns
The updated configuration.
Example
import dream_ets/config
import dream_ets/encoders
// Using built-in encoders
let assert Ok(table) = config.new("cache")
|> config.key_string()
|> config.value(encoders.string_encoder, encoders.string_decoder())
|> config.create()
Custom Encoders
For custom types like JSON or complex structures:
import gleam/json
import gleam/dynamic/decode
pub type User {
User(name: String, age: Int)
}
fn encode_user(user: User) -> dynamic.Dynamic {
json.object([
#("name", json.string(user.name)),
#("age", json.int(user.age)),
])
|> json.to_string
|> dynamic.from
}
fn decode_user() -> decode.Decoder(User) {
decode.string
|> decode.then(fn(str) {
// Parse JSON and decode User...
})
}
See Also
value_string()- Convenience function for string valueskey()- Set key encoder/decoder
pub fn value_string(
config: TableConfig(k, String),
) -> TableConfig(k, String)
Convenience: Set string value encoding
Configures the table to use string values. This is a convenience function
that calls value() with built-in string encoders.
Parameters
config: The table configuration to modify
Returns
The updated configuration with string value encoding.
Example
import dream_ets/config
let assert Ok(table) = config.new("cache")
|> config.key_string()
|> config.value_string()
|> config.create()
See Also
value()- For custom value typeskey_string()- For string keys
pub fn write_concurrency(
config: TableConfig(k, v),
enabled: Bool,
) -> TableConfig(k, v)
Enable or disable write concurrency
Optimizes the table for concurrent write operations. Enable this if multiple processes will be writing to the table simultaneously.
Parameters
config: The table configuration to modifyenabled:Trueto enable write concurrency,Falseto disable (default)
Returns
The updated configuration.
Example
import dream_ets/config
// Optimize for many simultaneous writers
let assert Ok(table) = config.new("metrics")
|> config.write_concurrency(True)
|> config.key_string()
|> config.value_string()
|> config.create()
When to Use
- Enable: High write traffic from multiple processes
- Disable: Single-threaded access or read-heavy workloads
Performance Impact
- Pro: Improves concurrent write performance
- Con: May increase memory usage slightly
Note
Write concurrency uses fine-grained locking. Best results when keys are well-distributed across the keyspace.