Access Shared Environment with Reader
Mix.install([
{:fun_park,
git: "https://github.com/JKWA/funpark_notebooks.git",
branch: "main"
}
])
Advanced Functional Programming with Elixir
|
Interactive Examples from Chapter 7 Advanced Functional Programming with Elixir. |
Build the Structures
```elixir
defmodule FunPark.Reader do
defstruct run: nil
def pure(value) do
%__MODULE__{run: fn _ -> value end}
end
def run(%__MODULE__{run: run}, environment) do
run.(environment)
end
end
```
Monad Behaviors
```elixir
defimpl FunPark.Monad, for: FunPark.Reader do
def map(%FunPark.Reader{run: run}, function) do
%FunPark.Reader{run: fn env -> function.(run.(env)) end}
end
def bind(%FunPark.Reader{run: run}, function) do
%FunPark.Reader{run: fn env ->
inner_reader = function.(run.(env))
inner_reader.run.(env)
end}
end
def ap(%FunPark.Reader{run: run_f}, %FunPark.Reader{run: run_v}) do
%FunPark.Reader{run: fn env -> run_f.(env).(run_v.(env)) end}
end
end
```
```elixir
def asks(function) do
%__MODULE__{run: function}
end
```
Avoid Prop Drilling
First, let’s generate a patron and a value:
alice = FunPark.Patron.make("Alice", 15, 130)
value = 2
And define a couple of simple functions:
square = fn n -> n * n end
message = fn {n, patron} -> "#{patron.name} has #{n}" end
We can call each one on its own:
square.(value)
message.({value, alice})
One strategy is to tunnel, having the square/1
accept and forward the patron information:
square_tunnel = fn {n, patron} -> {square.(n), patron} end
Now they can be piped together:
{value, alice} |> square_tunnel.() |> message.()
Instead, let’s update our message/1
function to take the number and retrieve the patron from the Reader
:
reader_message = fn n -> FunPark.Reader.asks(
fn patron -> "#{patron.name} has #{n}" end
) end
Now we can build the pipeline:
deferred_message = FunPark.Reader.pure(value)
|> FunPark.Monad.map(square)
|> FunPark.Monad.bind(reader_message)
We use run/2
to resolve deferred_message
with Alice:
FunPark.Reader.run(deferred_message, alice)
And we can just as easily switch patrons:
beth = FunPark.Patron.make("Beth", 16, 135)
FunPark.Reader.run(deferred_message, beth)
Dependency Injection
Let’s start again with Alice:
alice = FunPark.Patron.make("Alice", 15, 130)
We’ll define two services: one for production and one for testing:
prod_service = fn name -> "Hi, #{name}, from prod!" end
test_service = fn name -> "Hi, #{name}, from test!" end
The deferred_greeting
function applies a patron’s name to the injected service:
deferred_greeting = fn p -> FunPark.Reader.asks(& &1.(p.name)) end
Now we can construct a deferred greeting for Alice:
alice_greeting = deferred_greeting.(alice)
And inject the test service:
FunPark.Reader.run(alice_greeting, test_service)
Or the production service:
FunPark.Reader.run(alice_greeting, prod_service)
Shared Configuration
```elixir
def make_from_env(name) do
Reader.asks(fn config ->
make(name,
min_age: config.min_age,
min_height: config.min_height
)
end)
end
```
Our Apple Cart ride has a configuration:
apple_config = %{min_age: 10, min_height: 120}
And we can create a deferred_apple
—a ride that waits for its configuration:
deferred_apple = FunPark.Ride.make_from_env("Apple Cart")
Using the Reader, we run the deferred ride with its config:
apple = FunPark.Reader.run(deferred_apple, apple_config)