Effect separation
Like ordinary types give you a way to distinguish data, monads give you a way to distinguish effects.
Elixir
Here is an example in Elixir
, which is a nearly-pure functional language on top of Erlang. This example is derived from a real-world situation that occurs very often at my work.
def handle_call(:get_config_foo, _, state) do:
{:reply, state.foo, state}
end
def handle_call(:get_bar, _, state) do:
{:reply, state.bar, state}
end
def handle_call({:set_bar, bar}, _, state) do:
{:reply, :ok, %{state | bar: bar}}
end
This defines an API of a GenServer, which is a little Erlang node that holds some state
and allows you to query it as well as change it. The first call, :get_config_foo
reads an immutable config setting. The second set of calls, :get_bar
and {:set_bar, bar}
, gets and sets a mutable state variable.
How I wish I had monads here, to prevent the following bug:
def handle_call({:set_bar, bar}, _, state) do:
{:reply, :ok, %{state | foo: bar}}
end
Can you spot the mistake? Well, I just wrote a readonly value. Nothing in Elixir prevents this. You can't mark some parts of your GenServer state as readonly, and others als read-write.
Haskell: Reader and State
In Haskell you can use different monads to specify different kinds of effects. Here are readonly state (Reader
) and read-write state:
data Reader r a = Reader (r -> a)
data State s a = State (s -> (a, s))
Reader
allows you to access the config state r
to return some value a
. State
allows you to read the state, return some value and to modify the state. Both are monads, which essentially means that you can chain those state accesses sequentially in an imperative fashion. In Reader
you can first read one config setting, and then (based on that first setting), read another setting. In State
, you can read the state, and then (based on what you read) modify it in a further way. But you can never modify the state in Reader
while you execute it.
Deterministic effects
Let me repeat this. Binding several calls to Reader
assures you that you can never modify the reader state inbetween. If you have getConfigFoo1 :: Reader Config Foo1
and getConfigFoo2 :: Foo1 -> Reader Config Foo2
and you do getAllConfig = getConfigFoo1 >>= getConfigFoo2
, then you have certainty that both queries will run on the same Config
. Elixir does not have this feature and allows the above mentioned bug go unnoticed.
Other effects where this is useful is Writer
(write-only state, e.g. logging) and Either
(exception handling). When you have Writer
instead of Reader
or State
, you can be sure that your state is ever only appended to. When you have Either
, you know exactly the type of exceptions that can occur. This is all much better than using IO
for logging and exception handling.