Stellarmorphism
A stellar-themed algebraic data type DSL for Elixir that brings type-safe, elegant pattern matching and construction to your applications. Stellarmorphism provides a beautiful syntax for defining sum and product types with powerful recursion patterns.
Features
Phase 0: Core Foundation
- Planets (
defplanet) - Product types (structs) withorbitals(fields) - Stars (
defstar) - Sum types withlayerscontainingcorevariants - Fission - Type-safe pattern matching with star-prefixed syntax
- Fusion - Type-safe construction with star-prefixed syntax
- Asteroids - Lightweight recursive identifiers and helpers
Phase 1: Advanced Recursion
- Parameterized Types - Generic types with constraints
- Asteroid Recursion - Eager evaluation for immediate computation
- Rocket Recursion - Lazy evaluation for deferred/infinite structures
- Enhanced Constructors - Type parameters with runtime validation
- Mixed Recursion - Combine eager and lazy patterns in same structure
Installation
Add stellarmorphism to your list of dependencies in mix.exs:
def deps do
[
{:stellarmorphism, "~> 1.0"}
]
endThen add to your modules:
defmodule MyApp.Types do
use Stellarmorphism
endQuick Start
Basic Planets (Product Types)
defplanet User do
orbitals do
moon id :: String.t()
moon name :: String.t()
moon email :: String.t()
moon score :: integer()
end
end
# Usage
user = User.new(%{id: "1", name: "Alice", email: "alice@example.com", score: 100})Basic Stars (Sum Types)
defstar Result do
layers do
core Success, value :: any()
core Error, message :: String.t(), code :: integer()
end
end
# Construction
success = core(Success, value: "Operation completed")
error = core(Error, message: "Not found", code: 404)Fission (Pattern Matching)
message = fission Result, result do
core Success, value: data -> "Got: #{inspect(data)}"
core Error, message: msg, code: code -> "Error #{code}: #{msg}"
endFusion (Construction)
result = fusion Result, response do
{:ok, data} -> core Success, value: data
{:error, reason} -> core Error, message: reason, code: 500
endPhase 1: Advanced Recursion
Binary Tree with Asteroid Recursion (Eager)
defstar BinaryTree do
layers do
core Empty
core Leaf, value :: any()
core Node,
left :: asteroid(BinaryTree),
right :: asteroid(BinaryTree),
data :: any()
end
end
# Build tree - all nodes computed immediately
tree = core(Node,
left: asteroid(core(Leaf, value: 1)),
right: asteroid(core(Leaf, value: 3)),
data: 2
)
# Direct access (no function calls needed)
left_value = tree[:left][:value] # => 1Lazy Stream with Rocket Recursion (Lazy)
defstar LazyStream do
layers do
core Empty
core Cons,
head :: any(),
tail :: rocket(LazyStream)
end
end
# Build lazy stream - tail computed on demand
stream = core(Cons,
head: 1,
tail: rocket(fn ->
core(Cons,
head: 2,
tail: rocket(fn -> core(Empty) end)
)
end)
)
# Launch rockets to access values
tail_stream = launch(stream[:tail])
second_value = tail_stream[:head] # => 2Mixed Recursion Patterns
defstar HybridTree do
layers do
core Empty
core EagerNode,
value :: any(),
children :: list() # Immediate computation
core LazyNode,
value :: any(),
children :: rocket(list()) # Deferred computation
end
end
# Eager node - children computed immediately
eager = core(EagerNode,
value: "root",
children: [
asteroid(core(EagerNode, value: "child1", children: [])),
asteroid(core(EagerNode, value: "child2", children: []))
]
)
# Lazy node - children computed on demand
lazy = core(LazyNode,
value: "root",
children: rocket(fn ->
[core(LazyNode, value: "child1", children: rocket(fn -> [] end))]
end)
)
# Access patterns
eager_children = eager[:children] # Direct access
lazy_children = launch(lazy[:children]) # Launch requiredType Safety Benefits
Stellarmorphism eliminates namespace collisions by requiring star-prefixed syntax:
# Multiple stars can safely use same core names
defstar DatabaseResult do
layers do
core Success, rows :: list(), count :: integer()
core Error, message :: String.t(), sql_code :: String.t()
end
end
defstar HttpResult do
layers do
core Success, body :: String.t(), headers :: map()
core Error, message :: String.t(), http_code :: integer()
end
end
# Each star type is completely independent
db_result = core(Success, rows: data, count: 10)
http_result = core(Success, body: "response", headers: %{})
# Pattern matching with explicit star types
db_msg = fission DatabaseResult, db_result do
core Success, rows: rows, count: count -> "Found #{count} rows"
core Error, message: msg, sql_code: code -> "DB Error #{code}: #{msg}"
end
http_msg = fission HttpResult, http_result do
core Success, body: body, headers: _headers -> "Response: #{body}"
core Error, message: msg, http_code: code -> "HTTP Error #{code}: #{msg}"
endParameterized Types & Constraints
defstar BoundedList(max_size) when is_integer(max_size) and max_size > 0 do
layers do
core Empty, capacity :: integer()
core Partial,
items :: list(),
count :: integer(),
capacity :: integer()
core Full,
items :: list(),
capacity :: integer()
end
end
# Type constraint validation at construction
{:ok, small_list_type} = Types.apply_type_params(
BoundedList,
[{:max_size, quote(do: is_integer(max_size) and max_size > 0)}],
[5]
)
{:error, _} = Types.apply_type_params(
BoundedList,
[{:max_size, quote(do: is_integer(max_size) and max_size > 0)}],
[-1] # Invalid: negative size
)Performance Characteristics
Asteroid vs Rocket Trade-offs
Asteroids (Eager Evaluation):
- ✅ Higher memory usage upfront
- ✅ Faster access (no function calls)
- ✅ Immediate computation
- ❌ Not suitable for infinite structures
Rockets (Lazy Evaluation):
- ✅ Lower memory until launched
- ✅ Supports infinite/large structures
- ✅ Deferred expensive computations
- ❌ Slower access (requires function calls)
# Asteroid: All computed immediately
eager_tree = core(Node,
left: asteroid(expensive_computation()),
right: asteroid(another_computation()),
data: "root"
)
# Rocket: Computed only when needed
lazy_tree = core(Node,
left: rocket(fn -> expensive_computation() end),
right: rocket(fn -> another_computation() end),
data: "root"
)
# Access patterns
eager_left = eager_tree[:left] # Immediate
lazy_left = launch(lazy_tree[:left]) # Computed nowBenchmarks
Stellarmorphism includes comprehensive performance benchmarks to help you understand the trade-offs between asteroid (eager) and rocket (lazy) recursion patterns. The benchmark suite tests everything from basic performance to concurrency scaling and real-world scenarios.
Running Benchmarks
# Install dependencies first
mix deps.get
# Memory-safe benchmarks (recommended - prevents OOM)
mix run benchmarks/memory_safe_bench.ex
# Quick performance tests
mix run benchmarks/quick_bench.ex
# Run specific memory-safe tests
mix run benchmarks/memory_safe_bench.ex construction # Safe construction test
mix run benchmarks/memory_safe_bench.ex memory # Safe memory analysis
mix run benchmarks/memory_safe_bench.ex progressive # Progressive scale test
mix run benchmarks/memory_safe_bench.ex limits # Show safe limits
# Individual quick tests
mix run benchmarks/quick_bench.ex construction # Construction performance
mix run benchmarks/quick_bench.ex access # Access patterns
mix run benchmarks/quick_bench.ex traversal # Tree traversal
mix run benchmarks/quick_bench.ex memory # Memory usage
mix run benchmarks/quick_bench.ex comparison # Direct comparison
# Simple test to verify everything works
mix run benchmarks/simple_benchmark.ex simpleThe benchmark suite demonstrates key performance characteristics:
Construction Performance: Asteroids build structures immediately while rockets defer computation Access Patterns: Direct asteroid access vs rocket launch() overhead Memory Usage: Asteroids use more upfront memory, rockets scale better (⚠️ exponential growth at scale) Traversal Operations: Full structure processing comparisons
Memory Safety: Binary trees grow exponentially (2^depth nodes). Use memory-safe benchmarks to prevent out-of-memory conditions.
Benchmark Categories
🔥 Asteroid vs Rocket Performance
Tests the fundamental performance differences between eager and lazy evaluation:
- Construction Performance: Time to build data structures
- Access Patterns: Direct access vs launch() overhead
- Traversal Performance: Full structure processing
- Memory Usage: Memory consumption patterns
- Evaluation Strategies: Partial vs full evaluation
⚡ Concurrency Performance
Tests performance scaling from 1 to 32 processes:
- Construction Concurrency: Building structures in parallel
- Traversal Concurrency: Processing structures concurrently
- Pattern Matching: Concurrent fission operations
- Mixed Workloads: Real-world concurrent scenarios
- Rocket Evaluation: Lazy evaluation under concurrent load
📈 Scale Performance
Tests performance as data structure sizes grow:
- Tree Scaling: Binary trees from depth 3 to 15
- Stream Scaling: Lazy streams from 10 to 10,000 elements
- Memory Scaling: Memory usage analysis at scale
- Workload Scaling: Batch operations from 100 to 5,000 items
- Performance Degradation: Analysis of scaling bottlenecks
🏗️ Composite Real-World Scenarios
Tests realistic usage patterns:
- JSON Processing: Parsing and transforming nested JSON
- Error Handling Pipelines: Result types in processing chains
- Data Transformation: ETL-style workflows
- Caching Simulation: Lazy evaluation for cache systems
- Parser Combinators: Building and evaluating parse trees
- Web API Simulation: Request/response processing
Performance Characteristics
The benchmarks reveal key performance trade-offs:
Asteroid (Eager Evaluation):
- ✅ Faster access (no function calls)
- ✅ Predictable memory usage
- ✅ Better for frequently accessed data
- ❌ Higher upfront memory cost
- ❌ Not suitable for infinite structures
- ❌ All computation done immediately
Rocket (Lazy Evaluation):
- ✅ Lower initial memory usage
- ✅ Supports infinite/large structures
- ✅ Computation only when needed
- ✅ Better for streaming scenarios
- ❌ Slower access (requires launch())
- ❌ Unpredictable evaluation timing
Benchmark Results
Results are saved as HTML reports in benchmarks/results/ with:
- Detailed performance metrics
- Memory usage analysis
- Concurrency scaling charts
- Performance comparisons
- System configuration details
Performance Guidelines
Based on benchmark results:
- Use asteroids when: You need frequent access, bounded data, predictable performance
- Use rockets when: You have large/infinite data, infrequent access, streaming use cases
- Concurrency: Optimal performance typically at 4-8 processes for CPU-bound tasks
- Memory: Monitor usage carefully for deep structures (2^depth growth)
- Hybrid approaches: Combine both patterns based on access patterns
Utility Functions
Deep Evaluation
# Evaluate all nested rockets in a structure
nested_rockets = %{
level1: rocket(fn ->
%{level2: rocket(fn -> "deep_value" end)}
end)
}
fully_evaluated = Recursion.deep_launch(nested_rockets)
# => %{level1: %{level2: "deep_value"}}Rocket Depth Analysis
# Count nesting levels without evaluation
depth = Recursion.rocket_depth(nested_rockets) # => 2Type Information
# Check if a module uses parameterized types
Registry.is_parameterized?(MyBinaryTree) # => true
# Get type parameters
params = Registry.get_type_params(MyBinaryTree)
# => [{:t, nil}]Migration & Compatibility
Phase 0 code continues to work unchanged. You can gradually adopt Phase 1 features:
# Mix old and new approaches
mixed_structure = %{
legacy_field: "old_style",
eager_recursive: asteroid(%{data: "eager"}),
lazy_recursive: rocket(fn -> %{data: "lazy"} end)
}API Reference
Core Macros
defplanet/2- Define product types with orbitalsdefstar/2- Define sum types with layersfusion/3- Type-safe construction with pattern matchingfission/3- Type-safe pattern matchingcore/1,core/2- Construct star variantsasteroid/0,asteroid/1- Eager recursion helpersrocket/1- Create lazy evaluation structureslaunch/1- Evaluate rocket structures
Helper Modules
Stellarmorphism.Types- Type parameter extraction and validationStellarmorphism.Recursion- Asteroid/rocket utilitiesStellarmorphism.Constructors- Enhanced constructor generationStellarmorphism.Registry- Type registration and metadata
Examples
Real-world JSON Parser
defstar JsonValue do
layers do
core Null
core Bool, value :: boolean()
core Number, value :: number()
core String, value :: String.t()
core Array, elements :: list()
core Object, fields :: map()
end
end
# Parse with pattern matching
parse_json = fn input ->
fusion JsonValue, input do
nil -> core(Null)
bool when is_boolean(bool) -> core(Bool, value: bool)
num when is_number(num) -> core(Number, value: num)
str when is_binary(str) -> core(String, value: str)
list when is_list(list) -> core(Array, elements: list)
map when is_map(map) -> core(Object, fields: map)
end
end
# Use with fission
stringify = fn json_value ->
fission JsonValue, json_value do
core Null -> "null"
core Bool, value: true -> "true"
core Bool, value: false -> "false"
core Number, value: num -> to_string(num)
core String, value: str -> "\"#{str}\""
core Array, elements: elements -> "[#{Enum.join(elements, ", ")}]"
core Object, fields: fields -> "{#{inspect(fields)}}"
end
endFunctional Data Structures
# Persistent list with structural sharing
defstar PersistentList do
layers do
core Empty
core Cons,
head :: any(),
tail :: asteroid(PersistentList)
end
end
# Infinite sequence generator
defstar InfiniteSeq do
layers do
core Generator,
current :: any(),
next :: rocket(InfiniteSeq)
end
end
# Fibonacci sequence
fibonacci = fn ->
fib = fn a, b ->
core(Generator,
current: a,
next: rocket(fn -> fib.(b, a + b) end)
)
end
fib.(0, 1)
end
# Take first n elements
take = fn seq, n ->
if n <= 0 do
[]
else
case seq do
core(Generator, current: current, next: next_rocket) ->
[current | take.(launch(next_rocket), n - 1)]
end
end
end
fibs = fibonacci.()
first_10 = take.(fibs, 10) # [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]Contributing
Contributions welcome! Please read our contributing guidelines and submit pull requests to our GitHub repository.
License
MIT License - see LICENSE file for details.