TestHex.pmDocumentation

Freyja

Installation

Add freyja to your list of dependencies in mix.exs:

def deps do
  [
    {:freyja, "~> 0.1.1"}
  ]
end

What is Freyja?

Freyja is an Algebraic Effects system for Elixir, enabling you to write programs as pure functions that describe all their side effects as "effect" data structures. These effects are then interpreted by handlers, providing a clean separation between what your program does (the effects) and how it does it (the handlers).

0. tl;dr

Algebraic Effects and Handlers for Elixir. Both first-order and higher-order effects are supported. A with-like syntax is introduced to help with sequencing computations:

defhefty process_order(order_id) do
  # First-order effects are auto-lifted in hefty blocks.
  # Match failures will go to else
  %{price: order_price} = order <- EctoFx.query(Queries, :find_order, %{id: order_id})

  # ordinary = assignment & matching with else clause for failures
  {:ok, validated} = validate_order(order)

  # More effects - state tracking
  total_value <- State.get()
  new_total_value = total_value + order_price
  _ <- State.put(new_total_value)

  # Higher-order effect: transaction wrapping
  result <- EctoFx.transaction(
    hefty do
      _ <- EctoFx.update(Order.confirm_changeset(validated))
      _ <- EctoFx.insert(AuditLog.changeset(%{order_id: order_id, action: "confirmed"}))
      return({:confirmed, validated})
    end
  )

  return(result)
else
  # Handle pattern match failures (e.g., validate_order returned {:error, _})
  {:error, :invalid_items} -> return({:rejected, :invalid_items})
  {:error, reason} -> return({:rejected, reason})
catch
  # Handle thrown errors (e.g., from EctoFx operations)
  :db_connection_error -> return({:error, :database_unavailable})
  thrown -> return({:error, thrown})
end

1. What are Algebraic Effects?

Algebraic effects are plain data structures that describe something impure you want your program to do. Instead of performing I/O, mutating state, or throwing errors directly, a function can return an effect value such as “read the current state” or “write this log message”.

An algebraic effect system such as Freyja lets you build programs whose domain logic lives entirely in pure functions that emit these effect values. Separate handlers interpret the emitted data structures and decide how (or whether) to carry out the effects.

This separation has several benefits:

In short: describe your intentions as data, keep your business logic pure, and let Freyja orchestrate how and when effects run.

Further reading:“What is Algebraic about Algebraic Effects?” offers a gentle introduction to why they are called Algebraic Effects.

1.1 A real effect: Tagged State

Freyja is bundled with a number of Effects and Handlers - TaggedState is one of them - it gives access to "apparently mutable" (not really mutable!) state cells - from anywhere inside a nested stack of pure functions, without having to add any extra parameters to function signatures

TaggedState has a signature module Freyja.Effects.TaggedState, which defines some structs which represent the "operations" the effect supports, %TaggedState.Get{tag: <tag>} and %TaggedState.put{tag: <tag>, val: <val>}, and some "constructor functions" &TaggedState.get/1 and &TaggedState.put/2

# TaggedState: get/put state associated with a tag
defmodule Freyja.Effects.TaggedState do
  alias Freyja.Freer

  # Effect structs - plain data describing operations
  defmodule GetTagged do
    defstruct [:tag]
  end

  defmodule PutTagged do
    defstruct [:tag, :val]
  end

  # Constructor functions wrap structs in Freer.Impure via send_effect
  def get(tag), do: %GetTagged{tag: tag} |> Freer.send_effect()
  def put(tag, val), do: %PutTagged{tag: tag, val: val} |> Freer.send_effect()
end

The constructor functions like get(tag) and put(tag, val) build effect operation structs and wrap them in a minimal Freer.Impure structure using Freer.send_effect/1. This Impure struct is what gets interpreted by Handlers - see section 3.2 for more details on how send_effect and Impure work.

Remember that the effect structs themselves are just simple data - they are used to signal that a computation wants to do something, but they neither do anything nor say how a thing should be done.

%Freyja.Effects.TaggedState.GetTagged{tag: :cart}
%Freyja.Effects.TaggedState.PutTagged{tag: :cart, val: [:item_a, :item_b]}

Handlers decide exactly what to do with them — read from ETS, append to a log, store in a map, or something else entirely.

1.2 Define Your Own Effect Language

Most applications invent their own "impure verbs". With Freyja you can codify those verbs as effect structs instead of performing side effects immediately.

# Domain-specific storage effect
defmodule MyApp.Storage do
  alias Freyja.Freer
  
  defmodule Query do
    defstruct [:table, :id]
  end

  defmodule Change do
    defstruct [:table, :record]
  end

  # Constructor functions use send_effect to wrap structs in Freer.Impure
  def query(table, id), do: %Query{table: table, id: id} |> Freer.send_effect()
  def change(table, record), do: %Change{table: table, record: record} |> Freer.send_effect()
end

# Domain-specific notification effect
defmodule MyApp.Notifications do
  alias Freyja.Freer
  
  defmodule SendPush do
    defstruct [:user_id, :message]
  end

  def send_push(user_id, message), do: %SendPush{user_id: user_id, message: message} |> Freer.send_effect()
end

Your pure business logic can now "describe" what it needs. The con macro helps you compose (first-order) effectful computations using a familiar with-like syntax:

def checkout(cart, user) do
  con do
    product <- MyApp.Storage.query(:products, cart.product_id)

    if user.credit < product.price do
      Throw.throw_error(:insufficient_credit)
    else
      con do
        updated_user = %{user | credit: user.credit - product.price}
        _ <- MyApp.Storage.change(:users, updated_user)
        _ <- MyApp.Notifications.send_push(user.id, "Thanks for buying #{product.name}!")
        return({:ok, updated_user})
      end
    end
  end
end

At this point, checkout/2 is an entirely pure function —it only has pure domain logic and emits effect structs, while handlers will decide how to interpret them: hitting real services, wrapping DB access in transactions, or using mocks in tests.

case checkout(cart, user)
     |> MyApp.Storage.PostgreSQLHandler.run(db_connection)
     |> MyApp.Notifications.PigeonHandler.run(push_adapter)
     |> Throw.Handler.run()
     # eval returns only the result - run will return the full context
     |> Run.eval() do
  {:ok, updated_user} ->
    IO.inspect(updated_user, label: "User debited")

  {:error, :insufficient_credit} ->
    Logger.warn("Not enough credit")

  {:error, reason} ->
    Logger.error("Checkout failed: #{inspect(reason)}")
end

This illustrates how Freyja lets your domain logic stay pure while the handlers deal with the impure plumbing.

2. A Quick Tour: A short list of some cool things Algebraic Effects enable

Not nearly an exhaustive list, but there are IEx runnable examples for each case!

2.1 EctoFx: Taming dataabase interactions

The ecto_user_service.ex example shows how to build domain services that use Ecto effects for queries and mutations, while keeping domain logic completely testable without a database.

The Problem: Traditional Ecto code tightly couples domain logic to the database:

def create_user_with_profile(attrs) do
  Repo.transaction(fn ->
    user = Repo.insert!(User.changeset(attrs))
    profile = Repo.insert!(Profile.changeset(user, attrs))
    {user, profile}
  end)
end

This is hard to test without a database. With EctoFx effects:

defhefty register_user(attrs) do
  # Check if email already exists
  existing <- EctoFx.query(Queries, :find_user_by_email, %{email: attrs.email})

  result <-
    case existing do
      nil ->
        # Email not taken - create user and profile in transaction
        EctoFx.transaction(
          hefty do
            user <- EctoFx.insert(User.changeset(attrs))
            profile <- EctoFx.insert(Profile.changeset(user, attrs))
            return({user, profile})
          end
        )

      _user ->
        # Email already taken - return error via Throw
        Throw.throw_error({:email_taken, attrs.email})
    end

  return(result)
end

In tests - no database needed! Use EctoFx.TestHandler with stubbed queries:

state =
  EctoFx.TestHandler.new()
  |> EctoFx.TestHandler.stub_query(Queries, :find_user_by_email, %{email: "alice@test.com"}, nil)

outcome =
  EctoUserService.register_user(%{name: "Alice", email: "alice@test.com"})
  |> EctoFx.TestHandler.run(state)
  |> Lift.Algebra.run()
  |> Throw.Handler.run()
  |> Run.run()

assert {:ok, {%User{name: "Alice"}, %Profile{}}} = outcome.result

In production - real database with EctoFx.Handler:

outcome =
  EctoUserService.register_user(%{name: "Alice", email: "alice@example.com"})
  |> EctoFx.Handler.run(MyApp.Repo, %{Queries => :direct})
  |> Lift.Algebra.run()
  |> Throw.Handler.run()
  |> Run.run()

Benefits:

2.2 Coroutine-Based Programming

From the IEx runnable command_processor.ex example:

A Coroutine effect let you suspend and resume computations. Domain logic can be completely agnostic about how responses are gathered—interactive UI, CLI prompts, LLMs, or batch pipelines can all drive the same pure core.

Since effects are just simple data-structures you can use your effects as commands - and your whole system becomes command-driven with little effort.

Here's a simple coroutine-based command processor which repeatedly suspends, asking for the next command. You can feed it commands from a UI or CLI or, since your commands are just easily documented strucs, you can have an LLM build commands and AI enable your whole app for free:

defcon loop do
  # yield to outside the computation to ask for the next command
  command <- Coroutine.yield(:next_command)

  case command do
    %Storage.Query{} = effect ->
      handle_effect(effect)

    %Storage.Change{} = effect ->
      handle_effect(effect)

    %Notifications.SendPush{} = effect ->
      handle_effect(effect)

    :stop ->
      return(:stopped)

    other ->
      Throw.throw_error({:unknown_command, other})
  end
end

defconp handle_effect(effect) do
  _ <- effect
  loop()
end

# provide handlers for all the effects
builder = Freyja.Examples.CommandProcessor.builder()
# run the computation up to the yield
processor = Freyja.Run.run(builder)

commands = [
  Storage.query(:products, "A1"),
  Storage.change(:users, %{id: 1, name: "Ann"}),
  Notifications.send_push(1, "Hello!"),
  :stop
]
# repeatedly resume the computation with successive commands/effects
final_outcome = Enum.reduce(commands, processor, fn cmd, outcome ->
  Freyja.Run.resume(builder, outcome, cmd)
end)

Because commands are just effect structs, you can whitelist them for MCP tooling, log them, or feed them manually—no extra glue code required.

2.3 EffectLogger: Log, Replay, and Resume Anything

(a) Automatic Log Collection

By inserting EffectLogger.Handler.run/1 at the start of the Handler pipeline, you get full logs of every effect emitted—perfect for audit, tracing, or offline debugging.

outcome =
  con do
    config <- TaggedReader.ask(:config)
    starting <- State.get()
    updated = starting + config
    _ <- State.put(updated)
    return(updated)
  end
  |> EffectLogger.Handler.run(EffectLogger.Log.new())
  |> TaggedReader.Handler.run(%{config: 32})
  |> State.Handler.run(10)
  |> Run.run()

outcome.result # => 42
IO.inspect(outcome, pretty: true) # output below

Example output (abridged):

%Freyja.Run.RunOutcome{
  result: 42,
  outputs: %{
    Freyja.Effects.TaggedReader.Handler => %{config: 32},
    Freyja.Effects.EffectLogger.Handler => %Freyja.Effects.EffectLogger.Log{
      stack: [],
      queue: [
        %Freyja.Effects.EffectLogger.StepLogEntry{
          effects_stack: [],
          effects_queue: [
            %Freyja.Effects.EffectLogger.EffectLogEntry{
              sig: Freyja.Effects.TaggedReader,
              data: %Freyja.Effects.TaggedReader.AskTagged{tag: :config}
            }
          ],
          completed?: true,
          value: 32
        },
        %Freyja.Effects.EffectLogger.StepLogEntry{
          effects_stack: [],
          effects_queue: [
            %Freyja.Effects.EffectLogger.EffectLogEntry{
              sig: Freyja.Effects.State,
              data: %Freyja.Effects.State.Get{}
            }
          ],
          completed?: true,
          value: 10
        },
        %Freyja.Effects.EffectLogger.StepLogEntry{
          effects_stack: [],
          effects_queue: [
            %Freyja.Effects.EffectLogger.EffectLogEntry{
              sig: Freyja.Effects.State,
              data: %Freyja.Effects.State.Put{val: 42}
            }
          ],
          completed?: true,
          value: 10
        }
      ],
      allow_divergence?: false
    },
    Freyja.Effects.State.Handler => 42
  },
}

(b) Rerun to Debug (Even After Serialization)

builder =
  computation
  |> EffectLogger.Handler.run(log)
  |> State.Handler.run(0)

outcome = builder |> Run.run()

# Later: fix the code and rerun using the captured log
json = Jason.encode!(outcome)
decoded = Jason.decode!(json)

debug_outcome = Run.rerun(builder, decoded)

Run.rerun/2 will "run" a computation from "cold" logs (after a JSON serialization/deserialization roundtrip). Until the final step rerun doesn't really run anything other than the pure domain code - it supplies logged effect values to each step of the computation, so every step gets the exact same data that was logged during the failed computation run. At the final step (signalled by the :allow_divergence? flag in the Log), where an error may have been raised, it switches back to "new computation" mode and handles the effect normally, allowing bugfixed code to continue normally after the error.

you can try it out in IEx with: Freyja.Examples.EffectLoggerRerun:

buggy = Freyja.Examples.EffectLoggerRerun.build(:original)
buggy_outcome = buggy |> Freyja.Run.run()
buggy_outcome.result # => {:error, :validation_failed}
json = buggy_outcome |> Jason.encode!()

fixed = Freyja.Examples.EffectLoggerRerun.build(:patched)
fixed_outcome = Freyja.Run.rerun(fixed, Jason.decode!(json))
fixed_outcome.result # => {:ok, :ok}

(c) Cold Resume from Logs

builder = Freyja.Examples.EffectLoggerResume.build()
outcome = builder |> Freyja.Run.run()
{:suspend, prompt, _} = outcome.result
checkpoint = Jason.encode!(outcome)

# Later
decoded_checkpoint = Jason.decode!(checkpoint)
resumed = Freyja.Run.resume(builder, decoded_checkpoint, :new_value)
resumed.result # => {:done, :new_value}

EffectLogger’s serialized state is also enough to "cold" resume a coroutine from deserialized logs, even though the original continuation has been lost! See Freyja.Examples.EffectLoggerResume for a copy/pasteable builder demonstrating the pattern in IEx.

2.4 Change Capture with EctoFx

The ecto_change_capture.ex example demonstrates capturing intended database changes without immediately persisting them - enabling batch operations, dry-run mode, and audit logging.

The Pattern: Write simple per-record processing functions that use EctoFx.Changes to record changes, then use EctoFx.capture/1 to collect them without persisting:

# Simple per-record processing function
defhefty anonymize_user(user) do
  changeset = User.anonymize_changeset(user)

  # Record the change (captured, not persisted)
  _ <- EctoFx.Changes.update(changeset)

  # Also record an audit log entry
  audit_changeset = AuditLog.changeset(%{
    user_id: user.id,
    action: "anonymize",
    details: %{original_email: user.email}
  })
  _ <- EctoFx.Changes.insert(audit_changeset)

  return(Ecto.Changeset.apply_changes(changeset))
end

# Capture changes from processing multiple users
defhefty anonymize_users_with_capture(user_ids) do
  users <- EctoFx.query(Queries, :find_users_by_ids, %{ids: user_ids})

  # EctoFx.capture/1 collects all EctoFx.change calls without persisting
  {anonymized_users, captured_changes} <-
    EctoFx.capture(FxList.fx_map(users, &anonymize_user/1))

  return({anonymized_users, captured_changes})
end

The captured changes are returned as %{inserts: [...], updates: [...], deletes: [...]} containing Ecto changesets. Apply them in bulk within a transaction:

defhefty transactional_anonymize(user_ids) do
  EctoFx.transaction(
    hefty do
      users <- EctoFx.query(Queries, :find_users_by_ids, %{ids: user_ids})

      # Capture all changes without persisting
      {anonymized, changes} <-
        EctoFx.capture(FxList.fx_map(users, &anonymize_user/1))

      # Persist inserts in bulk (audit logs)
      _ <- EctoFx.insert_all(AuditLog, EctoFx.to_entries(changes.inserts))

      # Persist updates in bulk using upsert
      _ <- EctoFx.insert_all(
        User,
        EctoFx.to_entries(changes.updates),
        on_conflict: :replace_all,
        conflict_target: [:id]
      )

      return({anonymized, changes})
    end
  )
end

Use Cases:

2.5 TaggedReader: Stable Signatures When Requirements Change

The tagged_reader_dynamic_context.ex example demonstrates how algebraic effects keep function signatures stable when requirements change.

The Problem: In traditional code, adding context to a deep function requires changing every intermediate function's signature:

# Original
def generate_report(accounts), do: Enum.map(accounts, &summarize/1)
def summarize(account), do: %{name: account.name, spending: sum(account)}

# After requirements change - need greetings context
def generate_report(accounts, greetings), do: Enum.map(accounts, &summarize(&1, greetings))
def summarize(account, greetings), do: %{..., greeting: greetings[account.country]}

The Solution: With TaggedReader, the deep function simply asks for what it needs. No intermediate functions change:

# generate_report NEVER changes - works with any summarizer
defhefty generate_report(accounts, summarizer_fn) do
  FxList.fx_map(accounts, summarizer_fn)
end

# Version 1: Simple summary
defhefty summarize_spending(account) do
  total = sum_transactions(account.recent_transactions)
  return(%{name: account.name, recent_spending: total})
end

# Version 2: Requirements change! Need greeting - just ASK for it
defhefty summarize_with_greeting(account) do
  greetings <- TaggedReader.ask(:greetings)
  total = sum_transactions(account.recent_transactions)
  greeting = Map.get(greetings, account.country, "Hello!")
  return(%{name: account.name, recent_spending: total, greeting: greeting})
end

The context is provided at handler configuration time, completely decoupled from the function call chain:

# Version 1 - no context needed
TaggedReaderDynamicContext.build_v1(accounts)
|> Run.run()

# Version 2 - greetings provided at handler level
greetings = %{"UK" => "Cheerio!", "US" => "Howdy!", "DE" => "Guten Tag!"}

TaggedReaderDynamicContext.build_v2(accounts, greetings)
|> Run.run()
# => [%{name: "Alice", recent_spending: 41.49, greeting: "Cheerio!"}, ...]

Benefits:


3. How does it work

Let's look at a simple computation and develop an intuition for how it works:

con do
  x <- State.get()
  y <- Reader.ask()
  return(x + y)
end

This computation has a series of "steps", which correspond to lines inside the con block. We can read the steps as follows:

This is the "surface" interpretation of what's happening - and it's a reasonable approximation, but it hides considerable detail. Here's a more detailed reading:

The con block makes the "surface" interpretation easy, and that's deliberate - it's an abstraction built to give a convenient mental building block, but sometimes it's a good idea to understand the details, so in the next couple of sections we'll look at how the computation is broken down into steps, and those steps are exposed to an interpreter

3.1 The con and hefty Macros: Breaking down binds

The con and hefty macros provide a with-like syntax that rewrites to nested bind calls. This is similar to Haskell's do notation.

Simple Rewrite Rules

The macros apply a simple transformation:

  1. Effect binding (x <- effect()) becomes a bind call
  2. Pure binding (x = value) stays as a regular assignment
  3. The last expression is returned as-is (it must be a Freer.t() for con, or a Hefty.t() for hefty - so plain values should be wrapped with return(value))
  4. Functions with con or hefty bodies can be defined with defcondefhefty

Example: con macro expansion

# Input: con block with effect bindings
con do
  x <- State.get()
  y <- Reader.ask()
  return(x + y)
end

# Expands to: nested bind calls
State.get()
|> Freyja.Freer.bind(fn x ->
  Reader.ask()
  |> Freyja.Freer.bind(fn y ->
    return(x + y)
  end)
end)

Example: defcon macro expansion

The defcon macro defines a function with a con body. Pure = assignments are preserved inline within the continuation:

# Input
defcon double_state() do
  x <- State.get()
  doubled = x * 2
  _ <- State.put(doubled)
  return(doubled)
end

# Expands to
def double_state() do
  import Freyja.Freer.BaseOps

  State.get()
  |> Freyja.Freer.bind(fn x ->
    doubled = x * 2
    State.put(doubled)
    |> Freyja.Freer.bind(fn _ ->
      return(doubled)
    end)
  end)
end

Example: hefty macro expansion

The hefty macro works the same way but uses Hefty.bind instead:

# Input
defhefty anonymize_users_with_capture(user_ids) do
  users <- EctoFx.query(Queries, :find_users_by_ids, %{ids: user_ids})
  {results, changes} <- EctoFx.capture(FxList.fx_map(users, &anonymize_user/1))
  return({results, changes})
end

# Expands to
def anonymize_users_with_capture(user_ids) do
  import Freyja.Hefty, only: [return: 1]

  EctoFx.query(Queries, :find_users_by_ids, %{ids: user_ids})
  |> Freyja.Hefty.bind(fn users ->
    EctoFx.capture(FxList.fx_map(users, &anonymize_user/1))
    |> Freyja.Hefty.bind(fn {results, changes} ->
      return({results, changes})
    end)
  end)
end

Key Points

3.2 Freer - let's interpret some effects in IEx

Now we have a rough idea of how the computations in con and hefty blocks can be understood, and how they are expanded into normal Elixir code, let's develop that intuition further by manually performing the role of the interpreter and the Handlers the interpreter calls to deal with individual effects

We'll focus on first-order effects - they are effects which do not contain other effects in their data-structure, and are simpler to deal with - the con macro is used to expand first-order effects, while the hefty macro is used to expand higher-order effects.

Freyja uses a type called Freer to capture steps in a computation. Freer has two structs: There's Pure which wraps an ordinary-value, and represents a terminal state of a computation, and Impure which represents non-terminal states of a computation and holds:

defmodule Freer do

  defmodule Pure do
    defstruct val: nil

    @type t :: %__MODULE__{
            val: any
          }
  end

  defmodule Impure do
    defstruct sig: nil, data: nil, q: []

    @type t :: %__MODULE__{
            sig: atom,
            data: any,
            # should be list((any->freer))
            q: list((any -> freer))
          }
  end

  @type t() :: %Pure{} | %Impure{}
end

There are a few functions you will need to know about:

Let's look again at the expansion of the simple con block from above, and by manually playing the role of the interpreter and Handlers see how the computation gets represented as Freer and how the non-terminal Impure structs get repeatedly interpreted until there is only a terminal Pure struct.

con do
  x <- State.get()
  y <- Reader.ask()
  return(x + y)
end

and its expansion:

State.get()
|> Freyja.Freer.bind(fn x ->
  Reader.ask()
  |> Freyja.Freer.bind(fn y ->
    return(x + y)
  end)
end)

At the start we have State.get(), which is an "effect constructor" call - it uses Freer.send_effect to wrap a simple effect data structure in a minimal Impure structure - try it out yourself in IEx:

Freyja.Effects.State.get()

# %Freyja.Freer.Impure{
#  sig: Freyja.Effects.State,
#  data: %Freyja.Effects.State.Get{},
#  q: [&Freyja.Freer.pure/1]
#}

looking at the expansion again, we can see that the output of State.get() is immediately fed to a Freer.bind

here's the whole expansion without aliases, for copy/paste into IEx:

freer_1 = (
  Freyja.Effects.State.get()
  |> Freyja.Freer.bind(fn x ->
    Freyja.Effects.Reader.ask()
    |> Freyja.Freer.bind(fn y ->
      Freyja.Freer.return(x + y) end) end)
)

#%Freyja.Freer.Impure{
#  sig: Freyja.Effects.State,
#  data: %Freyja.Effects.State.Get{},
#  q: [&Freyja.Freer.pure/1, #Function<42.113135111/1 in :erl_eval.expr/6>]
#}

now you can see what the Freer.bind call has done - it's cheating, and hasn't done any work at all! It's just added the continuation function from its second argument to the end of the Impure's continuation q - but it has done nothing to interpret the %State.Get{} effect struct in the data field.

This is the heart of how Algebraic Effects work in Freyja - pure steps in domain calculations are expressed as a queue of continuations in Freer.Impure structs. Those pure steps perform no side-effects, and when they want a side-effect they return an effect operation struct to an Impure, along with a continuation which resumes the computation once the effect operation has been performed by the interpreter.

The data effect operation struct in the Impure describes some impure action that the program wants to do, without specifying anything about how the impure action is to be achieved - that is left entirely up to the interpreter and its Handlers. Let's continue playing the interpreter, and say that our State.Get operation is going to retrieve the value 5 from some state somewhere - so we pass that value to the first continuation in the q (which is &Freer.pure/1) which, as expected, just gives us the value 5 wrapped in a Freer.Pure struct.

freer_2 = (List.first(freer_1.q)).(5)
# %Freyja.Freer.Pure{val: 5}

Since Pure just wraps an ordinary-value, it needs no further interpretation, and we can pop the first continuation from the q, and pass the value we have - 5 - straight on to the next continuation from the q:

freer_3 = (Enum.at(freer_1.q, 1)).(5)

#%Freyja.Freer.Impure{
#  sig: Freyja.Effects.Reader,
#  data: %Freyja.Effects.Reader.Ask{},
#  q: [&Freyja.Freer.pure/1, #Function<42.113135111/1 in :erl_eval.expr/6>]
#}

Now we've got something different! We have a new effect to interpret, an Ask this time, and more continuations in the q to pass results to... This Ask struct was built by the Freyja.Effects.Reader.ask() call in the computation, inside the continuation passed to the first bind call.

Let's skip over the Freer.pure call we now have at the head of the queue, because we know what it's going to do (just wrap the value in Pure), and let's say interpreting the Ask returns a value of 15, because we're both the interpreter and the Handler and we can do that:

freer_4 = (Enum.at(freer_3.q, 1)).(15)
# %Freyja.Freer.Pure{val: 20}

And we have arrived - we called the final continuation, the function taking parameter y in the expansion corresponding to the last line of the con block, and it added together our two interpreted values and returned the result - and since return represents a terminal state and we have no more continuations on the q, the value is returned to the caller

This process we have followed is essentially what the Freyja interpreter does - it pattern-matches on the sig and data in Impure structs, finds a Handler to interpret the effect (producing an ordinary value), then passes that value to the next continuation in the queue. The Freyja.Run.impl module orchestrates this loop.


WIP below


3.2 Freer

3.3 Hefty


4. Available effects

4.1 first-order effects

4.2 higher-order effects


5. Building your own effects

References


License

MIT License