# `Skuld.Effects.Writer`
[🔗](https://github.com/mccraigmccraig/skuld/blob/main/lib/skuld/effects/writer.ex#L1)

Writer effect - accumulate a log during computation.

Supports both simple single-log usage and multiple independent logs via tags.

## Tag Convention

Tags should be module atoms — either the effect module itself (`Writer`)
for singleton logs, or a derived atom (`MyApp.Audit`, `Writer.Metrics`)
for namespaced logs. Module atoms are globally unique by construction,
eliminating accidental collisions between independent logs.

## Simple Usage (default tag)

    use Skuld.Syntax
    alias Skuld.Effects.Writer

    comp do
      _ <- Writer.tell("step 1")
      _ <- Writer.tell("step 2")
      :done
    end
    |> Writer.with_handler([], output: fn result, log -> {result, log} end)
    |> Comp.run!()
    #=> {:done, ["step 2", "step 1"]}

## Multiple Logs (explicit tags)

    comp do
      _ <- Writer.tell(:audit, "user logged in")
      _ <- Writer.tell(:metrics, {:counter, :login, 1})
      _ <- Writer.tell(:audit, "user viewed dashboard")
      :ok
    end
    |> Writer.with_handler([], tag: :audit, output: fn r, log -> {r, log} end)
    |> Writer.with_handler([], tag: :metrics)
    |> Comp.run!()
    #=> {:ok, ["user viewed dashboard", "user logged in"]}

## Per-tag dispatch

Each tag gets its own handler sig (a module atom) and compact operation
tuples. The tag is encoded in the sig, not in the operation args:

- `tell(msg)` → `Comp.effect(sig(tag), {Writer.Tell, msg})` — minimal 2-tuple
- `peek()`    → `Comp.effect(sig(tag), Writer.Peek)` — bare atom
- `set_log`   → `Comp.effect(sig(tag), {Writer.SetLog, log})` — internal

## Scoped Operations and Throw

The scoped operations (`listen`, `pass`, `censor`) use a peek-before/peek-after
pattern to calculate captured logs. This means on abnormal exit (throw):

- Logs written before the throw **persist** in state (not rolled back)
- `listen` does not capture partial logs - the throw propagates out
- `censor` does not apply its transform - logs leak out untransformed

This "fire-and-forget" semantic is intentional for logging (you typically want
to see logs leading up to an error).

# `__handle__`

Install Writer handler via catch clause syntax.

Accepts `nil`, `initial`, or `{initial, opts}`:

    catch
      Writer -> nil                          # empty initial
      Writer -> []                           # explicit empty initial
      Writer -> {[], output: fn r, l -> {r, Enum.reverse(l)} end}

# `censor`

```elixir
@spec censor(Skuld.Comp.Types.computation(), (list() -&gt; list())) ::
  Skuld.Comp.Types.computation()
```

Censor: transform all logs written during a computation.

## Examples

    Writer.censor(comp, &Enum.map(&1, fn m -> "[REDACTED]" end))
    Writer.censor(:audit, comp, &Enum.reverse/1)

# `censor`

```elixir
@spec censor(atom(), Skuld.Comp.Types.computation(), (list() -&gt; list())) ::
  Skuld.Comp.Types.computation()
```

# `clear`

```elixir
@spec clear(atom()) :: Skuld.Comp.Types.computation()
```

Clear the log.

## Examples

    Writer.clear()        # use default tag
    Writer.clear(:audit)  # use explicit tag

# `get_log`

```elixir
@spec get_log(Skuld.Comp.Types.env(), atom()) :: [term()]
```

Get the accumulated log from the environment.

## Examples

    Writer.get_log(env)          # use default tag
    Writer.get_log(env, :audit)  # use explicit tag

# `listen`

```elixir
@spec listen(Skuld.Comp.Types.computation()) :: Skuld.Comp.Types.computation()
```

Run a computation and capture its log output.

Returns `{result, captured_log}` where captured_log contains only
the messages written during the inner computation.

## Examples

    Writer.listen(comp)          # use default tag
    Writer.listen(:audit, comp)  # use explicit tag

# `listen`

```elixir
@spec listen(atom(), Skuld.Comp.Types.computation()) :: Skuld.Comp.Types.computation()
```

# `pass`

```elixir
@spec pass(Skuld.Comp.Types.computation()) :: Skuld.Comp.Types.computation()
```

Run a computation that returns `{value, log_transform_fn}`.

The transform function is applied to the logs written during the computation.

## Examples

    Writer.pass(comp)          # use default tag
    Writer.pass(:audit, comp)  # use explicit tag

# `pass`

```elixir
@spec pass(atom(), Skuld.Comp.Types.computation()) :: Skuld.Comp.Types.computation()
```

# `state_key`

# `tell_many`

```elixir
@spec tell_many([term()]) :: Skuld.Comp.Types.computation()
```

Tell multiple messages.

## Examples

    Writer.tell_many(["a", "b", "c"])           # use default tag
    Writer.tell_many(:audit, ["a", "b", "c"])   # use explicit tag

# `tell_many`

```elixir
@spec tell_many(atom(), [term()]) :: Skuld.Comp.Types.computation()
```

# `with_handler`

```elixir
@spec with_handler(Skuld.Comp.Types.computation(), list(), keyword()) ::
  Skuld.Comp.Types.computation()
```

Install a scoped Writer handler for a computation.

## Options

- `tag` - the log tag (default: `Skuld.Effects.Writer`)
- `output` - optional function `(result, final_log) -> new_result`
  to transform the result before returning.

## Examples

    # Simple usage with default tag
    comp do
      _ <- Writer.tell("step 1")
      _ <- Writer.tell("step 2")
      :done
    end
    |> Writer.with_handler()
    |> Comp.run!()
    #=> :done

    # Include final log in result
    comp do
      _ <- Writer.tell("step 1")
      :done
    end
    |> Writer.with_handler([], output: fn result, log -> {result, log} end)
    |> Comp.run!()
    #=> {:done, ["step 1"]}

    # With explicit tag
    comp do
      _ <- Writer.tell(:audit, "action 1")
      :done
    end
    |> Writer.with_handler([], tag: :audit, output: fn r, log -> {r, log} end)
    |> Comp.run!()
    #=> {:done, ["action 1"]}

    # Multiple logs
    comp do
      _ <- Writer.tell(:foo, "message 1")
      _ <- Writer.tell(:bar, "message 2")
      :done
    end
    |> Writer.with_handler([], tag: :foo)
    |> Writer.with_handler([], tag: :bar)
    |> Comp.run!()
    #=> :done

---

*Consult [api-reference.md](api-reference.md) for complete listing*
