We can't find the internet
Attempting to reconnect
Something went wrong!
Hang in there while we get back on track
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.