skuld_port
Port/adapter boundaries for Skuld — typed contracts, pluggable backends, and bridges between effectful and plain Elixir code. This is the hexagonal architecture layer: define interfaces, swap implementations.
What's included
Skuld.Effects.Port— dispatches function calls to pluggable backends via a registry. Supports plain resolvers (:direct, function handlers) and effectful resolvers (computation-returning modules with compile-time auto-detection via__port_effectful__?/0). Merges nested registries, supports dispatch logging.Skuld.Effects.Port.EffectfulFacade— generates typed effectful callers fromdefcallbackdeclarations. Single-module pattern for purely effectful boundaries; three-module pattern for mixed Plain/Effectful systems.Skuld.Adapter— bridges an effectful implementation (one whose functions returncomputation()) to a plain Elixir interface. Takes a contract, an impl module, and a handler stack — generates a module that satisfies the plain behaviour.Skuld.Adapter.EffectfulContract— generates effectful@callbackdeclarations from a DoubleDown contract. The contract module doubles as a__using__helper so implementations get@behaviourand the__port_effectful__?/0marker in one line.
The four directions
| Caller | Implementation | Mechanism |
|---|---|---|
| Plain Elixir | Plain Elixir | DoubleDown.ContractFacade |
| Plain Elixir | Effectful | Skuld.Adapter |
| Effectful | Plain Elixir | Port.with_handler + :direct |
| Effectful | Effectful | Port.with_handler + effectful module (auto-detected) |
Installation
def deps do
[
{:skuld_port, "~> 0.32"}
]
end
Example: single-module effectful boundary
# Contract + Facade in one module
defmodule MyApp.Payments do
use Skuld.Effects.Port.EffectfulFacade
defcallback charge(amount :: integer()) :: {:ok, receipt()} | {:error, term()}
end
# Implementation — one-liner with compile-time behaviour
defmodule MyApp.Payments.Stripe do
use Skuld.Syntax
use MyApp.Payments
@impl MyApp.Payments
defcomp charge(amount) do
{:ok, _} <- StripeAPI.create_charge(amount)
charge_id <- Fresh.uuid4()
{:ok, %{charge_id: charge_id}}
end
end
# Wire at runtime — auto-detected as effectful
MyApp.Payments.charge(99)
|> Port.with_handler(%{MyApp.Payments => MyApp.Payments.Stripe})
|> Throw.with_handler()
|> Comp.run!()
Further reading
- Port — resolvers, logging, key-based stubs
- EffectfulFacade — single-module and three-module patterns
- Adapter — bridging effectful → plain
- Hexagonal Architecture — full recipe with testing
See the architecture guide for how this fits into the Skuld ecosystem.