Powered by AppSignal & Oban Pro

Access Shared Environment with Reader

chapters/chap_07_reader.livemd

Access Shared Environment with Reader

Mix.install([
  {:fun_park,
    git: "https://github.com/JKWA/funpark_notebooks.git",
    branch: "main"
  }
])

Advanced Functional Programming with Elixir

https://www.joekoski.com/assets/images/jkelixir_small.jpg” alt=”Book cover” width=”120” /> 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)