It’s not a clickbait, there are no monads in Я. I still use monadic/comonadic adjectives just as a habit to describe stuff like binding and extension.
However, monads are overwemingly overrated! Here is why:
Yet another monad tutorial
I’m afraid refreshing some monad definitions is not something we can avoid here, but we are going to do it in our own way.
Imagine that there is some covariant functor called T
:
… and there is also a natural transformation so that if we can “produce” this functor structure from nothing (I
here stands for Identity
functor so that Supertype (I i) ~ i
):
… and another one that let us squash functor layers into one:
We are mostly interested in components of these natural transformations. If we would catch an essense of these components programming wise we would get something like this:
η[i]: into i (t i)
μ[i]: into (t (t i)) (t i)
into
here is a morphism, in Я it could be either an Arrow or an Attribute.
If this coherence condition is actually fulfilled…
μ[i] ∘ η[i] ≡ identity
Then T
is a monad!
(I actually skipped another coherence condition, but it’s trivial and comes from a property of natural transformation itself known as horizontal composition).
An example of monadic properties
Let’s check this equaility on example, let it be Maybe monad.
Optional value can be resolved by la operator:
(be "bye" `la` is) (Some "hi") ===> "hi"
(be "bye" `la` is) (None Unit) ===> "bye"
For the first natural transformation (η[i]
- “produce” this functor structure from nothing) we will wrap a value into Some
constructor:
η[i]: i `AR__` Maybe i
η[i] = Some
For the second one (μ[i]
- let us squash functor layers into one) we need to resolve Optional value twice:
μ[i]: (Maybe (Maybe i)) `AR__` (Maybe i)
μ[i] = None `la` (None `la` Some)
Checking this coherent condition is quite trivial:
(μ[i] ∘ η[i]) (None Unit)
===> (None `la` (None `la` Some)) `ha` Some) (None Unit)
===> (None `la` (None `la` Some)) (Some (None Unit))
===> (None `la` Some) (None Unit)
===> None Unit
(μ[i] ∘ η[i]) (Some Unit)
===> (None `la` (None `la` Some)) `ha` Some) (Some Unit)
===> (None `la` (None `la` Some)) (Some (Some Unit))
===> (None `la` Some) (Some Unit)
===> Some Unit
What about monad composition?
You probably have heard that monads do not compose - however, I think this statement is a bit misleading. You can compose monads of the same type - this is exactly what μ[i]
component is doing.
They mean a slightly different thing.
If we have a functor wrapped in another functor so that they form a functor composition - the result is functor too:
=> Covariant Endo Functor (AR) (State state)
=> Covariant Endo Functor (AR) (Stops reason)
=> Covariant Endo Functor (AR) (Stops reason `T'TT'I` State state)
Since monad is just a functor coupled with some natural transformations, we can use this a functor composition and get another monad, right? Well, WRONG!
The problem is that for each monad you need a special composition template, that’s why folks use monad transformers.
In Я similar approach is involved and it’s called jointed effects:
(Stops reason `T'TT'I` State state)
(Stops reason `JNT` State state)
So, what’s the diffrerence between these two type operators? Let’s look closely on what’s behind:
Supertype ((Stops r `T'TT'I` State s) i) ~ Stops r (State s i)
Supertype ((Stops r `JNT` State s) i) ~ Given s (Stops r (Stash s i))
Wow, we splitted State on Given and Stash and put Stops between them! It means that we always can pass an old state, but getting a new one is not garanteed.
Only the latter one could engage with both State and Stops effects (that’s why we say that they are jointed) using yok operator:
x: (Stops reason `JNT` State state) Unit
f: Unit `AR__` State state result
g: Unit `AR__` Stops reason result
h: Unit `AR__` (Stops reason `JNT` State state) Unit
x `yok` New `ha` f: (Stops reason `JNT` State state) result
x `yok` Try `ha` g: (Stops reason `JNT` State state) result
x `yok` Run `ha` h: (Stops reason `JNT` State state) result
It may remind you monad transformers:
x: StateT state (Maybe reason) Unit
f: Unit `AR__` State state result
g: Unit `AR__` Maybe reason result
h: Unit `AR__` StateT state (Maybe reason) Unit
x >>= lift . f: (StateT state (Maybe reason)) result
x >>= lift . g: (StateT state (Maybe reason)) result
x >>= h: (StateT state (Maybe reason)) result
But you don’t have to lift
since you can bind effect of different types.
Wait, what?
Let other functors participate!
It seems that in this chapter I mostly advertise monads rather than saying that they are bad (isn’t what you are hoping for?). So here is my statement:
Forget about monads. Don’t use this word anymore.
We can do better. It was just demonstrated that monad is a just a functor with two natural transformations and one coherence conditions. Instead of using such a packed up term, we are going to use its contents directly!
What if define some natural transformations to squash layers of different functors into one?
At first sight you may think that there are not many use cases when you can apply such a component (ψ[i]
). However, we truly need it when we deal with jointed effects:
x: (State state `JNT` Stops reason) Unit
g: Unit `AR__` Stops reason result
So that, if we want to bind g
to x
, we need the following constraints (for the sake of simplicity, I removed labels):
=> Covariant Endo Functor (AR) (Stops reason)
=> Covariant Endo Functor (AR) (Stops reason `JNT` State state)
=> Component (AR) (Stops reason `JNT` State state `T'TT'I` Stops reason) (Stops reason state `JNT` State state)
In this case, we are binding Stops effect (tt
) to jointed State and Stops (t
):
tt <= Stops reason
t <= Stops reason `JNT` State state
Now it’s time to generalize:
=> Covariant Endo Functor (AR) tt
=> Covariant Endo Functor (AR) t
=> Component (AR) (t `T'TT'I` tt) t
(`yok`): t a `AR__` (a `AR` tt a) `AR_` t o
Compare this type declaration to a widely known bind
operator from vanilla Haskell:
(>>=): t a `AR__` (a `AR` t a) `AR_` t o
So that, if you want to use some effect within another complex one, the only question you should ask is:
Does such a natural transformation exist?
Use natural transformations instead
I hope you got an idea - it’s more convenient to construct packed definitions (monads) from bricks (natural trasformations) instead of using packed ones and try to glue things together.
This interface is open and everyone could create their own bricks.
ψ[i]: (World `JNT` Stops r `T'TT'I` World) i `AR____` (World `JNT` Stops r) i
ψ[i]: (World `JNT` Stops r `T'TT'I` Stops r) i `AR__` (World `JNT` Stops r) i
ψ[i]: (World `JNT` State s `T'TT'I` World) i `AR____` (World `JNT` State s) i
ψ[i]: (World `JNT` State s `T'TT'I` State s) i `AR__` (World `JNT` State s) i
ψ[i]: (World `JNT` Stops r `JNT` State s `T'TT'I` State s) i `AR__` (World `JNT` Stops r `JNT` State s) i
ψ[i]: (World `JNT` Stops r `JNT` State s `T'TT'I` Stops r) i `AR__` (World `JNT` Stops r `JNT` State s) i
ψ[i]: (World `JNT` Stops r `JNT` State s `T'TT'I` World) i `AR____` (World `JNT` Stops r `JNT` State s) i