Current section

Files

Jump to
funx lib monad effect right.ex
Raw

lib/monad/effect/right.ex

defmodule Funx.Monad.Effect.Right do
@moduledoc """
[![Run in Livebook](https://livebook.dev/badge/v1/black.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2FJKWA%2Ffunx%2Fblob%2Fmain%2Flivebooks%2Fmonad%2Feffect%2Fright.livemd)
Represents the `Right` variant of the `Effect` monad, used to model a successful computation in an asynchronous context.
This module implements the following protocols:
- `Funx.Monad`: Implements the `bind/2`, `map/2`, and `ap/2` functions to handle monadic operations within an effectful, lazy execution context.
- `String.Chars`: Provides a `to_string/1` function to represent `Right` values as strings.
The `Right` effect allows the computation to proceed with successful values, supporting lazy, asynchronous tasks
and capturing execution context through the `Effect.Context` struct.
## Reader Operations
* `ask/1` – Returns the environment passed to `run/2` as a `Right`.
* `asks/2` – Applies a function to the environment passed to `run/2`, wrapping the result in a `Right`.
"""
alias Funx.Monad.{Effect, Either}
@enforce_keys [:effect, :context]
defstruct [:effect, :context]
@typedoc """
Represents an asynchronous computation that produces a `Right` value.
The `effect` function is typically a deferred task that takes an environment and returns a `Task`.
Since Elixir does not support parameterized `Task.t()` types, the return type is described as a union:
either a `Task.t()` or a plain `Either.Right.t(right)` for testability and flexibility.
The `context` carries telemetry and trace information used during execution.
"""
@type t(right) :: %__MODULE__{
effect: (term() -> Task.t()) | (term() -> Either.Right.t(right)),
context: Effect.Context.t()
}
@type t :: t(term())
@doc """
Creates a new `Right` effect.
The `pure/2` function wraps a value in the `Right` effect monad, representing an asynchronous success.
## Examples
iex> effect = Funx.Monad.Effect.Right.pure("success")
iex> Funx.Monad.Effect.run(effect)
%Funx.Monad.Either.Right{right: "success"}
"""
@spec pure(right, Effect.Context.opts_or_context()) :: t(right)
when right: term()
def pure(value, opts_or_context \\ []) do
%__MODULE__{
context: Effect.Context.new(opts_or_context),
effect: fn _env -> Task.async(fn -> Either.pure(value) end) end
}
end
@doc """
Returns a `Funx.Monad.Effect.Right` that yields the environment passed to `Funx.Monad.Effect.run/2`.
This is the Reader monad's equivalent of `ask`, giving access to the entire injected environment
for further computation.
## Example
iex> Funx.Monad.Effect.Right.ask()
...> |> Funx.Monad.map(& &1[:user])
...> |> Funx.Monad.Effect.run(%{user: "alice"})
%Funx.Monad.Either.Right{right: "alice"}
"""
@spec ask(Effect.Context.opts_or_context()) :: t(env)
when env: term()
def ask(opts_or_context \\ []) do
context = Effect.Context.new(opts_or_context)
%__MODULE__{
context: context,
effect: fn env ->
Task.async(fn -> Either.pure(env) end)
end
}
end
@doc """
Returns a `Funx.Monad.Effect.Right` that applies the given function to the environment passed to `Funx.Monad.Effect.run/2`.
This allows extracting a value from the environment and using it in an effectful computation,
following the Reader pattern.
## Example
iex> Funx.Monad.Effect.Right.asks(fn env -> env[:user] end)
...> |> Funx.Monad.bind(fn user -> Funx.Monad.Effect.right(user) end)
...> |> Funx.Monad.Effect.run(%{user: "alice"})
%Funx.Monad.Either.Right{right: "alice"}
"""
@spec asks((Effect.Context.t() -> result), Effect.Context.opts_or_context()) :: t(result)
when result: term()
def asks(f, opts_or_context \\ []) do
context = Effect.Context.new(opts_or_context)
%__MODULE__{
context: context,
effect: fn env ->
Task.async(fn -> Either.pure(f.(env)) end)
end
}
end
end
defimpl Funx.Monad, for: Funx.Monad.Effect.Right do
alias Funx.Errors.EffectError
alias Funx.Monad.{Effect, Either}
alias Effect.{Left, Right}
@spec map(Right.t(input), (input -> output)) :: Right.t(output)
when input: term(), output: term()
def map(%Right{effect: effect, context: context}, transform) do
updated_context = Effect.Context.promote_trace(context, "map")
%Right{
context: updated_context,
effect: fn env ->
Task.async(fn ->
case Effect.run(%Right{effect: effect, context: context}, env) do
%Either.Right{right: value} ->
try do
Either.pure(transform.(value))
rescue
e -> Either.left(EffectError.new(:map, e))
end
%Either.Left{} = left ->
left
end
end)
end
}
end
@spec bind(Right.t(input), (input -> Effect.t(left, output))) :: Effect.t(left, output)
when input: term(), output: term(), left: term()
def bind(%Right{effect: effect, context: context}, kleisli_fn) do
promoted_context = Effect.Context.promote_trace(context, "bind")
%Right{
context: promoted_context,
effect: fn env ->
Task.async(fn ->
case Effect.run(%Right{effect: effect, context: context}, env) do
%Either.Right{right: value} ->
try do
next = kleisli_fn.(value)
Effect.run(next, env)
rescue
e -> Either.left(EffectError.new(:bind, e))
end
%Either.Left{} = left ->
left
end
end)
end
}
end
@spec ap(Right.t((input -> output)), Right.t(input)) :: Right.t(output)
when input: term(), output: term()
@spec ap(Right.t(), Left.t(left)) :: Left.t(left)
when left: term()
def ap(%Right{effect: effect_func, context: context_func}, %Right{
effect: effect_value,
context: context_val
}) do
merged_context = Effect.Context.merge(context_func, context_val)
promoted_context = Effect.Context.promote_trace(merged_context, "ap")
%Right{
context: promoted_context,
effect: fn env ->
Task.async(fn ->
with %Either.Right{right: func} <-
Effect.run(%Right{effect: effect_func, context: context_func}, env),
%Either.Right{right: value} <-
Effect.run(%Right{effect: effect_value, context: context_val}, env) do
try do
Either.pure(func.(value))
rescue
e -> Either.left(EffectError.new(:ap, e))
end
else
%Either.Left{} = left -> left
end
end)
end
}
end
def ap(%Right{}, %Left{effect: eff, context: context}) do
promoted_context = Effect.Context.promote_trace(context, "ap")
%Left{
context: promoted_context,
effect: fn env -> eff.(env) end
}
end
end
defimpl Funx.Tappable, for: Funx.Monad.Effect.Right do
alias Funx.Monad.{Effect, Either}
alias Effect.Right
@spec tap(Right.t(value), (value -> any())) :: Right.t(value) when value: term()
def tap(%Right{effect: eff, context: context}, func) when is_function(func, 1) do
promoted_context = Effect.Context.promote_trace(context, "tap")
%Right{
context: promoted_context,
effect: fn env ->
Task.async(fn ->
case Effect.run(%Right{effect: eff, context: context}, env) do
%Either.Right{right: value} = result ->
func.(value)
result
%Either.Left{} = left ->
left
end
end)
end
}
end
end