Home Posts Post Search Tag Search

Advanced Functional Elixir - 07 - Access Shared Envrionment with Reader
Published on: 2026-04-10 Tags: elixir, Blog, Advanced Functional Programming, FunPark, Monads, Reader

7. Access Shared Environment with Reader

We will not look into deferred computation - define now run later. The role of the Reader monad is to defer computation and give it an environment. The most important part is to allow access to a read-only shared environment.

Build the Structures

This will have a pure/1 like Identity but will be called run/2 instead of extract/1.

defmodule FunPark.Reader do
  @enforce_keys [:run]
  defstruct [:run]

  def pure(value), do: %__MODULE__{run: fn _env -> value end}

  def run(%__MODULE__{run: f}, env), do: f.(env)
end

So for this we are storing the value directly. It will wrap the logic in a thunk - a function that defers computation until the environment is available.

Monad Behaviors

Now we need to implement the monadic behaviors for Reader: map/2, bind/2 and ap/2.

defimpl FunPark.Monad, for: FunPark.Reader do
  alias FunPark.Reader

  def map(%Reader{run: f}, func),
    do: %Reader{run: fn env -> func.(f.(env)) end}

  def bind(%Reader{run: f}, func),
    do: %Reader{run: fn env -> func.(f.(env)).run.(env) end}

  def ap(%Reader{run: f_func}, %Reader{run: f_value}),
    do: %Reader{run: fn env -> f_func.(env).(f_value.(env)) end}
end

Each of these is like the Identity but in these cases it threads it through the environment.


asks/1 is Readers special sauce:

defmodule FunPark.Reader do
  @enforce_keys [:run]
  defstruct [:run]

  def pure(value), do: %__MODULE__{run: fn _env -> value end}

  def run(%__MODULE__{run: f}, env), do: f.(env)

  def ask, do: %__MODULE__{run: fn env -> env end}

  def asks(func), do: %__MODULE__{run: func}
end

Avoid Prop Drilling

There might be times where data needs to be available deep within a computation. Let’s see what happens when we pass that information manually - and then see how Reader provides a better alternative.

Run It

iex(1)> alice = FunPark.Patron.make("Alice", 15, 130)
%FunPark.Patron{
  id: 3138,
  name: "Alice",
  age: 15,
  height: 130,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(2)> value = 2
2
iex(3)> square = fn n -> n * n end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(4)> message = fn {n, patron} -> "#{patron.name} has #{n}" end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(5)> square.(value)
4
iex(6)> message.({value, alice})
"Alice has 2"
iex(7)> square_tunnel = fn {n, patron} -> {square.(n), patron} end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(8)> {value, alice} |> square_tunnel.() |> message.()
"Alice has 4"
iex(9)> reader_message = fn n -> FunPark.Reader.asks(
...(9)> fn patron -> "#{patron.name} has #{n}" end
...(9)> ) end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(10)> (deferred_message = FunPark.Reader.pure(value)
...(10)> |> FunPark.Monad.map(square)
...(10)> |> FunPark.Monad.bind(reader_message))
%FunPark.Reader{
  run: #Function<1.118414679/1 in FunPark.Monad.FunPark.Reader.bind/2>
}
iex(11)> FunPark.Reader.run(deferred_message, alice)
"Alice has 4"
iex(12)> beth = FunPark.Patron.make("Beth", 16, 135)
%FunPark.Patron{
  id: 3714,
  name: "Beth",
  age: 16,
  height: 135,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(13)> FunPark.Reader.run(deferred_message, beth)
"Beth has 4"

Okay so we first started out by building a Patron and then a square/1 and message/2 function. We saw that we could use them both and how one takes 1 argument and the other takes a Patron and a message.


How do we now pipe them together? We we could make a single anonymous function that takes the value and the Patron and then manually bundle the value together with the patron so later functions can still access both. message/1.


Okay so now lets try this an other way. We can change the message to use the asks to take the number and retrieve the Patron from the reader. That then can be passed from the map to the bind.

Reader lets us defer access to shared context until we actually need it.
pure/1 lifts a plain value into a Reader.
map/2 transforms the Reader’s result while keeping the same environment.
bind/2 chains Reader-producing computations while passing the same environment through each step.
ask/0 returns the whole environment, and asks/1 derives a value from it.
run/2 finally supplies the environment and executes the computation.

Dependency Injection

Dependency injection decouples a component from its service.

Run It

iex(14)> alice = FunPark.Patron.make("Alice", 15, 130)
%FunPark.Patron{
  id: 3778,
  name: "Alice",
  age: 15,
  height: 130,
  ticket_tier: :basic,
  fast_passes: [],
  reward_points: 0,
  likes: [],
  dislikes: []
}
iex(15)> prod_service = fn name -> "Hi, #{name}, from prod!" end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(16)> test_service = fn name -> "Hi, #{name}, from test!" end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(17)> deferred_greeting = fn p -> FunPark.Reader.asks(& &1.(p.name)) end
#Function<42.39164016/1 in :erl_eval.expr/6>
iex(18)> alice_greeting = deferred_greeting.(alice)
%FunPark.Reader{run: #Function<42.39164016/1 in :erl_eval.expr/6>}
iex(19)> FunPark.Reader.run(alice_greeting, test_service)
"Hi, Alice, from test!"
iex(20)> FunPark.Reader.run(alice_greeting, prod_service)
"Hi, Alice, from prod!"

Here we define two service functions, one for test and one for production. The deferred_greeting/1 function takes a patron and returns a Reader that waits for a service function as its environment. When we call deferred_greeting.(alice), the patron’s name is captured, but the choice of service is still deferred. Later, Reader.run/2 supplies either test_service or prod_service, and the stored computation uses that service to generate the final greeting.

Shared Configuration

Reader is also very good at accessing configuration data. We will build a make_from_config/1 that will allow us to set a way to change a Rides min_age and min_height

# Ride
  def make_from_env(name) do
    FunPark.Reader.asks(fn config ->
      make(name)
      |> change(%{
        min_age: Map.get(config, :min_age, 0),
        min_height: Map.get(config, :min_height, 0)
      })
    end)
  end

Run It

iex(21)> apple_config = %{min_age: 10, min_height: 120}
%{min_age: 10, min_height: 120}
iex(22)> deferred_apple = FunPark.Ride.make_from_env("Apple Cart")
%FunPark.Reader{run: #Function<19.111038010/1 in FunPark.Ride.make_from_env/1>}
iex(23)> apple = FunPark.Reader.run(deferred_apple, apple_config)
%FunPark.Ride{
  id: 4034,
  name: "Apple Cart",
  min_age: 10,
  min_height: 120,
  wait_time: 0,
  online: true,
  tags: []
}

This is designed to allow access to keep separated the way to change and apply those changes to a Domain, and to keep the way in which a service might send those values to change. You can give a String for a name and then a Map of values to change.

What You’ve Learned

Here are some tests with different values for the last set of run it.

iex(24)> bad_apple_config = %{min_age: 3, min_hieght: 23}
%{min_age: 3, min_hieght: 23}
iex(25)> bad_apple = FunPark.Reader.run(deferred_apple, bad_apple_config)
%FunPark.Ride{
  id: 4098,
  name: "Apple Cart",
  min_age: 3,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}
iex(26)> bad_apple = FunPark.Reader.run(deferred_apple, [])
** (BadMapError) expected a map, got: []
    (elixir 1.18.0) lib/map.ex:535: Map.get([], :min_age, 0)
    (fun_park 0.1.0) lib/fun_park/ride.ex:58: anonymous fn/2 in FunPark.Ride.make_from_env/1
    iex:26: (file)
iex(26)> bad_apple = FunPark.Reader.run(deferred_apple, %{})
%FunPark.Ride{
  id: 4226,
  name: "Apple Cart",
  min_age: 0,
  min_height: 0,
  wait_time: 0,
  online: true,
  tags: []
}

There is no Eq and Ord within Reader as it needs to be run to get anything from it.