Contexir
🧠 Context-Oriented Programming for Elixir — composable layers for dynamic, context-aware behavior.
Contexir brings Context-Oriented Programming (COP) semantics to Elixir. It lets you define layers that dynamically refine how your modules behave, based on runtime context — without modifying the original code.
✨ Features
- 🧩 Layered Behavior — define context-sensitive extensions for existing functions.
- 🪞 Execution Plan — predictable order of execution (
around,before,after). - ⚙️ Composable Layers — group and reuse layers (
use_layers). - 🧠 Dynamic Activation — activate layers with
Contexir.with_layers/2. - 💡 Pure Functional Design — no global mutation, fully scoped per process.
- 🧰 Simple API — use regular Elixir syntax; no complex macros required.
🚀 Installation
Add Contexir to your mix.exs:
def deps do
[
{:contexir, "0.2.0"}
]
end
Then fetch the dependency:
mix deps.get
🧩 Basic Example
import Contexir.Layer
require Contexir
defmodule Account do
use Contexir
def withdraw(acc, amt, _ctx) do
IO.puts("primary")
%{acc | balance: acc.balance - amt}
end
end
deflayer LoggingLayer do
defpartial Account.withdraw(acc, amt, ctx), mode: :before do
IO.puts("[BEFORE] Logging withdrawal of #{amt}")
end
defpartial Account.withdraw(acc, amt, ctx), mode: :around do
IO.puts("[AROUND] Starting transaction")
result = continue(Account, :withdraw, [acc, amt, ctx])
IO.puts("[AROUND] Finished transaction")
result
end
defpartial Account.withdraw(_acc, _amt, _ctx), mode: :after do
IO.puts("[AFTER] Done.")
end
end
Contexir.with_layers(
[LoggingLayer],
Account.withdraw(%{balance: 1000}, 100, %{})
)
Output:
[AROUND] Starting transaction
[BEFORE] Logging withdrawal of 100
primary
[AFTER] Done.
[AROUND] Finished transaction
🧭 Execution Model
When multiple layers are active, the execution order follows this pattern:
| Mode | Direction | Description |
|---|---|---|
| around | outer → inner | Each around wraps the next layer. Must call continue/3. |
| before | outer → inner | Runs before the primary function. |
| primary | — | The original function being refined. |
| after | inner → outer | Runs after the primary returns. |
Example for [A, B] active layers:
A:around
B:around
A:before
B:before
primary
B:after
A:after
B:around end
A:around end
⚙️ Layer Composition
Layers can include other layers:
deflayer SecureLayer do
use_layers [AuthLayer, LoggingLayer]
end
Contexir.with_layers(
[SecureLayer],
Checkout.submit(cart, %{user: user})
)
Layers can also declare relationships that are validated before activation:
deflayer SecureCheckout do
requires Authentication
conflicts_with GuestCheckout
before Audit
end
Contexir.Layer.resolve([Authentication, SecureCheckout, Audit])
#=> {:ok, [Authentication, SecureCheckout, Audit]}
🧠 Declarative Context Activation
Use defcontext when layers should be selected from context values:
defcontext AppContext do
layer MobileLayout, when: &(&1.network == :cellular)
layer LowBattery, when: &(&1.battery < 20)
end
Contexir.with_context(
AppContext,
%{network: :cellular, battery: 10},
Page.render(%{layout: :desktop}, %{})
)
⚡ Task Propagation
Regular BEAM processes do not inherit Contexir context or active layers. Use
Contexir.Task when a task should run with the caller's current Contexir scope:
Contexir.Context.with_scope([TraceLayer], %{request_id: "req-123"}, fn ->
task =
Contexir.Task.async(fn ->
Contexir.with_layers([], Worker.run("job", Contexir.Context.current()))
end)
Contexir.Task.await(task)
end)
📚 Runnable Examples
The examples/ directory contains small scripts for the main APIs:
mix run examples/basic_layers.exs
Available examples:
examples/basic_layers.exsexamples/composition_resolution.exsexamples/declarative_context.exsexamples/task_propagation.exsexamples/checkout_flow.exs— end-to-end example combining context rules, composition validation, layered dispatch, and task propagation.
💡 Why Context-Oriented Programming?
Traditional OOP or FP decomposition struggles with runtime behavioral variation — when behavior must adapt to context (e.g., user role, request origin, environment).
COP solves this by:
- Separating context-dependent behavior into layers.
- Activating those layers dynamically, without polluting core logic.
- Allowing clean, composable runtime adaptation.
Contexir brings these ideas to Elixir — leveraging the BEAM’s process isolation and pure data flow.
📦 Project Goals
- Keep the model simple and functional.
- Serve as a foundation for runtime adaptation libraries in Elixir.
- Explore dynamic system composition patterns (adaptive services, domain-specific contexts, etc.).
🧰 Roadmap
- Layer predicates for context-based activation
- Declarative context activation
- Task scope propagation
- Precedence ordering and cycle detection
- Debug/tracing integration
📄 License
Unlicense