Getting Started
View Source< How Effects Work | Up: Introduction | Index | Syntax In Depth >
Add skuld to your dependencies:
def deps do
[{:skuld, "~> 0.27"}]
endYour first computation
A computation describes what should happen. Write it with comp do:
defmodule MyApp.Examples do
use Skuld.Syntax
defcomp greet(name) do
trimmed = String.trim(name)
if trimmed == "" do
{:error, :empty_name}
else
{:ok, "Hello, #{trimmed}!"}
end
end
endRun it with Comp.run!():
iex> MyApp.Examples.greet("Alice") |> Comp.run!()
{:ok, "Hello, Alice!"}The defcomp macro defines a function that returns a computation (a
2-arity function). Comp.run!() executes it. If the computation
produces a sentinel (an error or suspension), run! raises.
For more control, use Comp.run() which returns {result, env}:
iex> {result, _env} = MyApp.Examples.greet("Alice") |> Comp.run()
iex> result
{:ok, "Hello, Alice!"}Adding effects
Effects let your computation describe side effects without performing them. Here's one that generates IDs and reads configuration:
defcomp register(params) do
config <- Reader.ask() # read configuration
id <- Fresh.fresh_uuid() # generate a unique ID
{:ok, %{id: id, name: params.name, tier: config.default_tier}}
endTo run it, install handlers for each effect:
register(%{name: "Alice"})
|> Reader.with_handler(%{default_tier: :free})
|> Fresh.with_uuid7_handler()
|> Comp.run!()
# => {:ok, %{id: "018f9b8c-...", name: "Alice", tier: :free}}Production vs test
The same computation runs with different handlers:
alias Skuld.Repo
defcomp register(params) do
config <- Reader.ask()
id <- Fresh.fresh_uuid()
{:ok, user} <- Repo.insert(User.changeset(%{id: id, name: params.name, tier: config.default_tier}))
{:ok, user}
end
# Production
register(%{name: "Alice"})
|> Reader.with_handler(%{default_tier: :free})
|> Fresh.with_uuid7_handler()
|> Port.with_handler(%{Skuld.Repo.Effectful => MyApp.Repo.Ecto})
|> Throw.with_handler()
|> Comp.run!()
# Test — same code, deterministic, no database
register(%{name: "Alice"})
|> Reader.with_handler(%{default_tier: :free})
|> Fresh.with_test_handler()
|> Repo.InMemory.with_handler(Repo.InMemory.new())
|> Throw.with_handler()
|> Comp.run!()Repo.InMemory is a closed-world in-memory store. Records inserted
during the test are immediately readable by subsequent Repo.get /
Repo.get_by calls — no mocks, no stubs.
What next?
- How Effects Work — the conceptual model
- State, Reader & Writer — foundational effects
- Port — dispatching to external implementations
- Repo — built-in database contract
< How Effects Work | Up: Introduction | Index | Syntax In Depth >