skuld_port

View Source

Port > | Umbrella →

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 from defcallback declarations. Single-module pattern for purely effectful boundaries; three-module pattern for mixed Plain/Effectful systems.
  • Skuld.Adapter — bridges an effectful implementation (one whose functions return computation()) 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 @callback declarations from a DoubleDown contract. The contract module doubles as a __using__ helper so implementations get @behaviour and the __port_effectful__?/0 marker in one line.

The four directions

CallerImplementationMechanism
Plain ElixirPlain ElixirDoubleDown.ContractFacade
Plain ElixirEffectfulSkuld.Adapter
EffectfulPlain ElixirPort.with_handler + :direct
EffectfulEffectfulPort.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

See the architecture guide for how this fits into the Skuld ecosystem.


Port > | Umbrella →