GenObject
A library for creating stateful objects backed by GenServer processes with inheritance support.
GenObject provides a macro-based DSL for defining object-like structures that maintain state in GenServer processes. Objects support field access, updates, lazy operations, and merging. The library integrates seamlessly with the Inherit library to provide powerful inheritance modeling capabilities.
Features
- Stateful Objects: Objects backed by GenServer processes with automatic lifecycle management
- Field Operations: Get, put, and merge operations with both synchronous and asynchronous variants
- Lazy Operations: Functions that compute values based on current object state
- Inheritance Support: Integration with the Inherit library for object inheritance patterns
- Process Safety: All operations are process-safe through GenServer messaging
- Performance: Asynchronous variants for high-performance scenarios
Installation
Add gen_object to your list of dependencies in mix.exs:
def deps do
[
{:gen_object, "~> 0.1.0"},
{:inherit, "~> 0.1.0"} # For inheritance features
]
end
Quick Start
Basic Object Definition
defmodule Person do
use GenObject, [
name: "",
age: nil,
email: nil
]
end
# Create a new person object
person = Person.new(name: "Alice", age: 30)
# Returns: %Person{name: "Alice", age: 30, email: nil, pid: #PID<...>}
Field Access
# Get the complete object
current_person = Person.get(person)
# Returns: %Person{name: "Alice", age: 30, email: nil, pid: #PID<...>}
# Get a specific field (more efficient)
name = Person.get(person, :name)
# Returns: "Alice"
# Can also use PID directly
age = Person.get(person.pid, :age)
# Returns: 30
Field Updates
# Synchronous update (returns updated object)
person = Person.put(person, :age, 31)
person = Person.put(person.pid, :email, "alice@example.com")
# Asynchronous update (returns :ok immediately)
:ok = Person.put!(person, :age, 32)
:ok = Person.put!(person.pid, :name, "Alice Smith")
# Verify async updates
updated_person = Person.get(person)
Lazy Updates
Lazy updates allow you to compute new values based on the current object state:
# Increment age based on current value
person = Person.put_lazy(person, :age, fn p -> p.age + 1 end)
# Create display name from existing fields
person = Person.put_lazy(person, :display_name, fn p ->
"#{p.name} (#{p.age})"
end)
# Asynchronous lazy update
:ok = Person.put_lazy!(person, :age, fn p -> p.age + 1 end)
Merging Multiple Fields
# Synchronous merge
person = Person.merge(person, %{
name: "Alice Johnson",
age: 35,
email: "alice.johnson@example.com",
location: "San Francisco"
})
# Asynchronous merge
:ok = Person.merge!(person, %{age: 36, location: "New York"})
# Lazy merge based on current state
person = Person.merge_lazy(person, fn p ->
age_group = if p.age < 18, do: "minor", else: "adult"
%{
age_group: age_group,
can_vote: p.age >= 18,
display_name: "#{p.name} (#{age_group})"
}
end)
Inheritance with the Inherit Library
GenObject uses the Inherit library to provide powerful inheritance modeling:
Basic Inheritance
defmodule Animal do
use GenObject, [
name: "",
species: "",
age: 0
]
def speak(%__MODULE__{} = animal) do
"#{animal.name} makes a sound"
end
end
defmodule Dog do
use GenObject
inherit Animal, [
breed: "",
trained: false
]
# Override parent method
def speak(%__MODULE__{} = dog) do
"#{dog.name} barks! Woof!"
end
def sit(%__MODULE__{trained: true} = dog) do
Dog.put(dog, :position, :sitting)
end
def sit(%__MODULE__{trained: false} = dog) do
{:error, "#{dog.name} is not trained to sit"}
end
end
# Dog inherits all fields from Animal plus its own
dog = Dog.new(
name: "Rex",
species: "Canis lupus",
breed: "Labrador",
age: 3,
trained: true
)
# Use inherited and own methods
Dog.speak(dog) # "Rex barks! Woof!"
Dog.sit(dog) # Updates position to :sitting
Multi-level Inheritance
defmodule LivingThing do
use GenObject, [
alive: true,
birth_date: nil
]
end
defmodule Animal do
use GenObject
inherit LivingThing, [
name: "",
species: ""
]
end
defmodule Mammal do
use GenObject
inherit Animal, [
warm_blooded: true,
fur_color: nil
]
end
defmodule Dog do
use GenObject
inherit Mammal, [
breed: "",
trained: false
]
end
# Dog inherits from the entire chain
dog = Dog.new(
name: "Buddy",
species: "Canis lupus",
breed: "Golden Retriever",
fur_color: "golden",
birth_date: ~D[2020-01-15],
trained: true
)
Complex Inheritance Patterns
defmodule Vehicle do
use GenObject, [
make: "",
model: "",
year: nil,
mileage: 0
]
def drive(%__MODULE__{} = vehicle, distance) do
Vehicle.put_lazy(vehicle, :mileage, fn v -> v.mileage + distance end)
end
end
defmodule Car do
use GenObject
inherit Vehicle, [
doors: 4,
fuel_type: :gasoline
]
def honk(%__MODULE__{} = car) do
"#{car.make} #{car.model} honks: BEEP BEEP!"
end
end
defmodule ElectricCar do
use GenObject
inherit Car, [
battery_capacity: 0,
charge_level: 100,
fuel_type: :electric # Override parent default
]
def charge(%__MODULE__{} = car, amount) do
ElectricCar.put_lazy(car, :charge_level, fn c ->
min(100, c.charge_level + amount)
end)
end
# Override parent method
def drive(%__MODULE__{} = car, distance) do
car = super(car, distance) # Call parent implementation
# Reduce charge based on distance
ElectricCar.put_lazy(car, :charge_level, fn c ->
max(0, c.charge_level - div(distance, 10))
end)
end
end
# Create an electric car with full inheritance chain
tesla = ElectricCar.new(
make: "Tesla",
model: "Model 3",
year: 2023,
battery_capacity: 75,
doors: 4
)
# Use methods from all levels of inheritance
tesla = ElectricCar.drive(tesla, 100) # Inherited and overridden
tesla = ElectricCar.charge(tesla, 20) # Own method
message = Car.honk(tesla) # Parent method
Advanced Usage
Custom GenServer Callbacks
You can override GenServer callbacks while still using GenObject functionality:
defmodule TimestampedObject do
use GenObject, [
data: nil,
created_at: nil,
updated_at: nil
]
# Override init to set timestamps
def init(opts) do
now = DateTime.utc_now()
opts = opts
|> Keyword.put(:created_at, now)
|> Keyword.put(:updated_at, now)
super(opts) # Call GenObject's init
end
# Override handle_call to update timestamps
def handle_call({:put, field, value}, from, object) do
result = super({:put, field, value}, from, object)
case result do
{:reply, updated_object, state} ->
updated_state = TimestampedObject.put(state, :updated_at, DateTime.utc_now())
{:reply, updated_object, updated_state}
other -> other
end
end
end
Supervision
GenObjects can be supervised like any GenServer:
defmodule MyApp.ObjectSupervisor do
use Supervisor
def start_link(_opts) do
Supervisor.start_link(__MODULE__, [], name: __MODULE__)
end
def init([]) do
children = [
{Person, [name: "Default Person"]},
{Dog, [name: "Default Dog", breed: "Mixed"]}
]
Supervisor.init(children, strategy: :one_for_one)
end
end
Performance Considerations
- Use asynchronous operations (
put!/3,merge!/2, etc.) when you don't need the return value - Use
get/2for single fields instead ofget/1when you only need one field - Batch multiple updates using
merge/2instead of multipleput/3calls - Lazy operations are computed synchronously, so complex computations may block
API Reference
Creation and Lifecycle
YourModule.new/1- Create a new objectYourModule.close/1- Stop the object process
Field Access
YourModule.get/1- Get complete object stateYourModule.get/2- Get specific field value
Field Updates
YourModule.put/3- Update field synchronouslyYourModule.put!/3- Update field asynchronouslyYourModule.put_lazy/3- Update field with function synchronouslyYourModule.put_lazy!/3- Update field with function asynchronously
Bulk Operations
YourModule.merge/2- Merge multiple fields synchronouslyYourModule.merge!/2- Merge multiple fields asynchronouslyYourModule.merge_lazy/2- Merge with function synchronouslyYourModule.merge_lazy!/2- Merge with function asynchronously
All functions accept either a PID or an object struct containing a :pid field. Replace YourModule with your actual module name (e.g., Person, Dog, etc.).
Contributing
- Fork the repository
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new Pull Request
License
This project is licensed under the MIT License - see the LICENSE.md file for details.