Skuld.Comp.CompBlock (skuld v0.1.12)

View Source

The comp macro for monadic do-notation style effect composition.

Provides comp, defcomp, and defcompp macros that transform arrow notation (<-) into Skuld.Comp.bind chains.

Usage

import Skuld.Comp.CompBlock

comp do
  x <- State.get()
  y = x + 1
  _ <- State.put(y)
  return(y)
end

Syntax

  • x <- effect() - bind the result of an effectful computation
  • x = expr - pure variable binding (unchanged)
  • return(value) - lift a pure value (optional - values are auto-lifted)
  • Last expression is auto-lifted if not already a computation

Auto-Lifting

Non-computation values are automatically wrapped in pure():

comp do
  x <- State.get()
  _ <- if x > 5, do: Writer.tell(:big)  # nil auto-lifted when false
  x * 2  # final expression auto-lifted (no return needed)
end

Else Clause

You can add an else clause to handle pattern match failures in <- bindings:

comp do
  {:ok, x} <- maybe_returns_error()
  return(x)
else
  {:error, reason} -> return({:failed, reason})
  other -> return({:unexpected, other})
end

When a pattern in <- fails to match, the else clause handles the unmatched value.

Catch Clause

You can add a catch clause for error handling:

comp do
  x <- State.get()
  _ <- if x < 0, do: Throw.throw(:negative), else: Comp.pure(:ok)
  return(x * 2)
catch
  :negative -> return(0)
  other -> return({:error, other})
end

When an error is thrown, it's matched against the catch patterns. If no pattern matches and there's no catch-all, the error is re-thrown.

Combined Else and Catch

Both clauses can be used together. The else must come before catch:

comp do
  {:ok, x} <- might_fail_match()
  _ <- might_throw_error(x)
  return(x)
else
  {:error, reason} -> return({:match_failed, reason})
catch
  :some_error -> return(:caught_throw)
end

Semantic ordering: catch(else(body)). This means:

  • else handles pattern match failures from the main computation
  • catch handles throws from both the main computation AND the else handler

Installing Handlers

Use the pipe operator with Module.with_handler/2 to install scoped handlers:

comp do
  x <- State.get()
  y <- Reader.ask()
  return(x + y)
end
|> State.with_handler(0)
|> Reader.with_handler(:config)

Handlers are applied in order (first in pipe = innermost).

Function Definitions

defcomp increment() do
  x <- State.get()
  _ <- State.put(x + 1)
  return(x + 1)
end

defcompp private_helper() do
  ctx <- Reader.ask()
  return(ctx.value)
end

Function definitions also support else and catch:

defcomp safe_get() do
  {:ok, x} <- fetch_value()
  return(x)
else
  {:error, _} -> return(:default)
catch
  :serious_error -> return(:fallback)
end

Summary

Functions

Create a computation using do-notation style syntax.

Define a public function whose body is a comp block.

Define a private function whose body is a comp block.

Functions

comp(clauses)

(macro)

Create a computation using do-notation style syntax.

Transforms arrow bindings (<-) into Skuld.Comp.bind chains. Regular assignments (=) are preserved as-is.

Supports optional else and catch clauses.

Example

comp do
  x <- State.get()
  y = x * 2
  _ <- State.put(y)
  return(y)
end

With else and catch clauses:

comp do
  {:ok, x} <- risky_operation()
  return(x)
else
  {:error, reason} -> return({:failed, reason})
catch
  :error -> return(:default)
end

defcomp(call_ast, clauses)

(macro)

Define a public function whose body is a comp block.

Supports optional else and catch clauses.

Example

defcomp fetch_and_increment() do
  x <- State.get()
  _ <- State.put(x + 1)
  return(x)
end

defcomp safe_fetch() do
  {:ok, x} <- dangerous_op()
  return(x)
else
  {:error, _} -> return(:default)
catch
  :error -> return(:fallback)
end

defcompp(call_ast, clauses)

(macro)

Define a private function whose body is a comp block.

Supports optional else and catch clauses.

Example

defcompp internal_helper() do
  x <- Reader.ask()
  return(x.config)
end