Current section
Files
Jump to
Current section
Files
lib/monad/behaviour/map.ex
defmodule Funx.Monad.Behaviour.Map do
@moduledoc """
Behaviour for map operations across monad DSLs.
Map is a **universal functor operation** that works identically across all monads.
Unlike `bind`, which handles failure, `map` simply transforms values when they exist.
The same `map` module can be used in Either DSL, Maybe DSL, List, IO, or any
other monad - the transformation logic is completely generic.
## Contract
```elixir
@callback map(value :: any(), opts :: keyword(), env :: keyword()) :: any()
```
## Arguments
- `value` - The value to transform
- `opts` - Keyword list of options (module-specific configuration)
- `env` - Environment/context from DSL (for Reader-like dependency injection)
## Return Values
Map should return **the transformed value directly** as a plain value.
The DSL handles wrapping the result in the appropriate monad type.
**Important**: Unlike `Bind`, map does not return Either, Maybe, or result tuples.
It returns plain values because map is about transformation, not control flow.
## Semantic Rules
1. **Arguments strictly ordered**: value, opts, env
2. **May use env** for Reader-like dependency injection (only way to access env - functions cannot)
3. **Pure transformation** - should not fail (use Behaviour.Bind for operations that can fail)
4. **Returns plain value** - not wrapped in monad (DSL handles wrapping)
5. **Monad-agnostic** - same transformation works across all monad DSLs
## Monad Universality
The beauty of `map` is that it works the same way in every monad:
- **Either**: `Right(value)` → apply map → `Right(transformed)`; `Left` is skipped
- **Maybe**: `Just(value)` → apply map → `Just(transformed)`; `Nothing` is skipped
- **List**: `[a, b, c]` → apply map → `[f(a), f(b), f(c)]`
The same `map` module behaves consistently across all these contexts.
## Examples
### Basic Transformation
```elixir
defmodule Double do
@behaviour Funx.Monad.Behaviour.Map
@impl true
def map(value, _opts, _env) when is_number(value) do
value * 2
end
end
# Works identically in Either DSL
use Funx.Monad.Either
either 21 do
map Double
end
#=> %Right{right: 42}
# Works identically in Maybe DSL
use Funx.Monad.Maybe
maybe 21 do
map Double
end
#=> %Just{value: 42}
# Same transformation, different monad contexts
```
### With Options
```elixir
defmodule Multiplier do
@behaviour Funx.Monad.Behaviour.Map
@impl true
def map(value, opts, _env) when is_number(value) do
factor = Keyword.get(opts, :factor, 1)
value * factor
end
end
# In Either DSL
either 10 do
map {Multiplier, factor: 5}
end
#=> %Right{right: 50}
# In Maybe DSL
maybe 10 do
map {Multiplier, factor: 5}
end
#=> %Just{value: 50}
```
### Composable Transformations
```elixir
defmodule ToUpperCase do
@behaviour Funx.Monad.Behaviour.Map
@impl true
def map(value, _opts, _env) when is_binary(value) do
String.upcase(value)
end
end
defmodule AddPrefix do
@behaviour Funx.Monad.Behaviour.Map
@impl true
def map(value, opts, _env) when is_binary(value) do
prefix = Keyword.get(opts, :prefix, "")
prefix <> value
end
end
# Compose maps in Either
either "hello" do
map ToUpperCase
map {AddPrefix, prefix: ">> "}
end
#=> %Right{right: ">> HELLO"}
# Same composition works in Maybe
maybe "hello" do
map ToUpperCase
map {AddPrefix, prefix: ">> "}
end
#=> %Just{value: ">> HELLO"}
```
"""
@doc """
Transforms a value.
Arguments:
* value - The current value in the pipeline
* opts - Module-specific options passed in the DSL
* env - Environment/context from the DSL (for dependency injection)
Returns the transformed value directly (not wrapped in a monad).
Examples:
# Simple transformation
def map(value, _opts, _env) do
value * 2
end
# With options
def map(value, opts, _env) do
multiplier = Keyword.get(opts, :multiplier, 1)
value * multiplier
end
# Using env for dependency injection
def map(value, _opts, env) do
formatter = Keyword.get(env, :formatter)
formatter.format(value)
end
"""
@callback map(value :: any(), opts :: keyword(), env :: keyword()) :: any()
end