Is it actually true, that Monads are basically simply flatMaps and thats it?
In essence a monad is anything that just has two functions (though some languages break them up into more, but this is the basics) and follows a few laws:
- First you need to have the type, it can be anything that can contain anything else, 0 or more times, so an optional (0 or 1 times), a list (0 or more times), a tuple (N times), a map (0 or more times), or anything you can think of or create that can hold âsomethingâ (like a future in java/c++/rust/javascript/etc⊠is a monad type for example).
- First function is the unit function, it just takes a value and wraps it in the type you specify, it is often named
unit
/return
/wrap
/etc⊠The ElixirEnum.wrap/2
is such a unit function if you pass it only a single value (it technically does more than just what unit should do, a realunit
in elixir is something like the list constructor like[42]
or{42}
or%{a: 42}
or so forth). - Second function is the bind function, often called
flat_map
(as it is in Elixir), it is just a function that takes an instance of a type (like an elixir list) and a function, and it calls that function on each element inside the monad and returns that same monadic type back (this is where Elixirâs flat_map breaks down, making it not monadic either, it just accepts returning a list), that list of monadic type instances returned are then flattened back down one level, so something like (in Elixir parlance)flat_map([1, 2, 3, 4], fn x when even?(x) -> [x, x]; _ -> [] end)
returns[2, 2, 4, 4]
andflat_map(%{a: 1, b: 2}, fn {key, value} -> %{key => value; value => key} end)
would return%{a: 1, b: 2, 1 => :a, 2 => :b}
and so forth. - The first law being that (Iâll use elixirâs terminology for these of
wrap
/flat_map
)wrap
when passed toflat_map
is the same as the original instance, I.E.something === flat_map(something, &wrap(&1, theType)
andflat_map(wrap(v), f) === f.(v)
(it is this why Elixirâs flat_map is not monadic, it breaks this law). - The second law being that the succession order of flat_mapping does not matter, I.E.
something |> flat_map(f) |> flat_map(g) === flat_map(something, fn x -> flat_map(f.(x), g) end)
So yes, that looks long but only because there are so many examples, more succinctly:
defmodule ListMonad do
def wrap(v), do: [v]
def flat_map([], _f), do: []
def flat_map([head, rest], f), do: f.(head) ++ flat_map(rest, f)
end
defmodule OkMonad do
def wrap(v), do: {:ok, v}
def flat_map(:error, _f), do: :error
def flat_map({:ok, v}, f), do: f.(v)
end
Which you could use like:
[1, 2, 3]
|> ListMonad.flat_map(&[&1*2]) # Result: [2, 4, 6]
|> ListMonad.flat_map(&if(&1>=4, do: [], else: [&1])) # Returns: [2, 4]
21
|> OkMonad.wrap() # Returns {:ok, 21}
|> OkMonad.flat_map(&{:ok, &1&2}) # Returns: {:ok, 42}
|> OkMonad.flat_map(&if(&1>30, do: :error, else: {:ok, &1})) # Returns: :error
|> OkMonad.flat_map(&{:ok, &1&2}) # Still returns just: :error
Any kind of container can become a monad if you can define anything that follows the above requirements. In a properly typed system you can make generic functions that work over it all, but in Elixir you need some way to define types, the easiest way is to just name them somehow, like via a module name ListMonad
/OkMonad
or via atoms passed in plain instances there-of (which is what Enum.wrap/2
does) or whatever.
But overall yeah, Monads are dead-stupid-simple, they just have a few rules that make them complex, but they are so very simple.
OK, thanks a lot for this detailed description. ^.^
This video explains it like mentioned, I think its very easy to understand:
Do you think its accurate?
Since he says at one point, that its always flatMap, just under different names.
P.S: I just saw, that this here is public, while I thought its our private chat
Not a clue, at work, I can rarely watch videoâs in general thus I tend to need text. ^.^;
Lol, all good.
I was curious about implementing the monad pattern in elixir so I went ahead and did so in a new playground
- Monad definition:
- Monad implementations for list/map/okerrortuples:
- And the tests (it already has itâs own built-in tests via
deftest
but here are explicit ones too to show usage):
@OvermindDL1 can split the posts into a new thread then make the thread a PM between you both (prob best to do that for any posts that are not related to the OP, ODL?)
Monads are functional so it is related to ReasonML, but not with phoenix yeah, Iâll break it out. Done. ^.^
Had some fun with with my monad protocol, I benchmarked itâs flat_map versus Enum.flat_map (via the awesome Benchee), and I did it with enum in two ways, both the âcorrectâ return, and the way that it would traditionally be done in Elixir (which is marked as the *_wrong
one as it is not following the flat_map laws properly, meaning it also returns a list instead of the actual object). I benched with a list, a map, and a map of a single element (named ValueMap
). The results:
Operating System: Linux
CPU Information: Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz
Number of Available Cores: 2
Available memory: 8.011072 GB
Elixir 1.6.0-dev
Erlang 20.0
Benchmark suite executing with the following configuration:
warmup: 5.00 s
time: 5.00 s
parallel: 1
inputs: List, Map, ValueMap
Estimated total run time: 1.50 min
Benchmarking with input List:
Benchmarking Elixir.Enum.flat_map...
Benchmarking Elixir.Enum.flat_map_wrong...
Benchmarking ExCore.Monad.flat_map...
Benchmarking with input Map:
Benchmarking Elixir.Enum.flat_map...
Benchmarking Elixir.Enum.flat_map_wrong...
Benchmarking ExCore.Monad.flat_map...
Benchmarking with input ValueMap:
Benchmarking Elixir.Enum.flat_map...
Benchmarking Elixir.Enum.flat_map_wrong...
Benchmarking ExCore.Monad.flat_map...
##### With input List #####
Name ips average deviation median
ExCore.Monad.flat_map 3.71 M 0.27 Όs ±1318.59% 0.20 Όs
Elixir.Enum.flat_map 3.67 M 0.27 Όs ±1058.86% 0.20 Όs
Elixir.Enum.flat_map_wrong 3.64 M 0.27 Όs ±1075.87% 0.20 Όs
Comparison:
ExCore.Monad.flat_map 3.71 M
Elixir.Enum.flat_map 3.67 M - 1.01x slower
Elixir.Enum.flat_map_wrong 3.64 M - 1.02x slower
##### With input Map #####
Name ips average deviation median
ExCore.Monad.flat_map 4.15 M 0.24 Όs ±11150.89% 0.0 Όs
Elixir.Enum.flat_map_wrong 1.33 M 0.75 Όs ±310.86% 0.70 Όs
Elixir.Enum.flat_map 0.67 M 1.49 Όs ±1624.04% 1.00 Όs
Comparison:
ExCore.Monad.flat_map 4.15 M
Elixir.Enum.flat_map_wrong 1.33 M - 3.12x slower
Elixir.Enum.flat_map 0.67 M - 6.19x slower
##### With input ValueMap #####
Name ips average deviation median
ExCore.Monad.flat_map 7.35 M 0.136 Όs ±518.59% 0.120 Όs
Elixir.Enum.flat_map 3.42 M 0.29 Όs ±10604.27% 0.0 Όs
Elixir.Enum.flat_map_wrong 1.84 M 0.54 Όs ±2503.76% 0.30 Όs
Comparison:
ExCore.Monad.flat_map 7.35 M
Elixir.Enum.flat_map 3.42 M - 2.15x slower
Elixir.Enum.flat_map_wrong 1.84 M - 3.99x slower
In addition, mine works with any type the user wants to write an implementation for, like for the Erlang :array
module, you could do that (where you cannot with Enum
). ^.^
But as you can see, mine is faster in every case, âbarelyâ so with lists (basically the same implementation that Enum already uses I donât doubt) to surprisingly and significantly faster with maps (even the âfastâ version that skips much of Enumâs work is still much slower). In addition, mine returns a list when a list is input and a map when a map is input, as flat_map/2
is supposed to be doing. ^.^
/me is tempted to implement more category theory functions at this point other than just monad, could probably run circles around a lot of the existing Elixir code (though the elixir code could be made faster if anyone tried)
/me is still unsure why on earth Enum.flat_map/2
does not return the same type that it took in, like whaaaâŠ
EDIT: Oh, and the benchmarking code:
defmodule Helpers do
def do_flat_map({k, v}), do: %{k => v, v => k}
def do_flat_map(v), do: [v, v]
def do_flat_map_wrong({k, v}), do: [{k, v}, {v, k}]
def do_flat_map_wrong(v), do: [v, v]
end
inputs = %{
"List" => [:a, :b, :c, :d],
"Map" => %{a: 1, b: 2, c: 3, d: 4},
"ValueMap" => ExCore.Monad.wrap(42, %{}),
}
actions = %{
"Elixir.Enum.flat_map" => fn input -> Enum.flat_map(input, &Helpers.do_flat_map/1) end,
"Elixir.Enum.flat_map_wrong" => fn input -> Enum.flat_map(input, &Helpers.do_flat_map_wrong/1) end,
"ExCore.Monad.flat_map" => fn input -> ExCore.Monad.flat_map(input, &Helpers.do_flat_map/1) end,
}
Benchee.run actions, inputs: inputs, time: 5, warmup: 5, print: %{fast_warning: false}
Shouldnât Enum always return the type enumerable instead of a list? I know you can convert the list back to the enumerable but it seems more logical to generate it from the beginning. So for example the types of filter
and flat_map
of course should be:
filter(t, (element -> as_boolean(term))) :: t
flat_map(t, (element -> t)) :: t
Considering it is in an Enum
module, very probably it should, but the enumerable type in Elixir is a list, so⊠We need better enumeration modules, perhaps of a category type system style. ^.^
Honestly, who actually does something like Enum.map
over something that they do not know the type of, you generally already know if it is a map or a list or a struct or something, so why not just call the right module and get rid of the indirection and get rid of the enum module?
I agree with you, and honestly donât miss an equivalent enumerable
type and Enum
module in Erlang. The specific type modules have the traversing functions needed.
Likewise, I often use the base erlang modules like :lists
and :maps
surprisingly often in Elixir, just because Elixirâs List
and Map
is missing some pretty basic functions that those have. ^.^;
I made an Access clone, and a lens
helper for it! ^.^
Lens usage:
import ExCore.Access, only: [lens: 1]
o0a = %{a: 42}
assert l0a = lens a
assert l0b = lens [:a]
assert 42 = l0a.get(o0a)
assert 42 = l0b.get(o0a)
o1a = %{a: %{b: 42}}
assert l1a = (lens a.b)
assert l1b = (lens [:a][:b])
assert l1c = (lens a[:b])
assert l1d = (lens [:a].b)
assert l2a = (lens {:c})
assert l2b = (lens {:c, 32})
assert l2c = (lens {:c, %{b: 16}}.b)
assert l2d = (lens a{:c, %{d: 12}}.d)
assert 42 = l1a.get(o1a)
assert 42 = l1b.get(o1a)
assert 42 = l1c.get(o1a)
assert 42 = l1d.get(o1a)
assert nil == l2a.get(o1a)
assert 32 = l2b.get(o1a)
assert 16 = l2c.get(o1a)
assert 12 = l2d.get(o1a)
o2a = %{a: 21}
o2b = %{a: %{b: 21}}
assert l2a = (lens a)
assert l2b = (lens a.b)
assert l2c = (lens a{:c, %{d: 21}}.d)
assert %{a: 42} = l2a.get_and_update(o2a, &(&1*2))
assert %{a: %{b: 42}} = l2b.get_and_update(o2b, &(&1*2))
assert %{a: %{b: 21, c: %{d: 42}}} = l2c.get_and_update(o2b, &(&1*2))
Yep, you can even specify defaults inside the lens. ^.^
But essentially:
- A raw thing, like
a
is equal to[:a]
- A
[term]
usesterm
as the key - A
{term}
usesterm
as the key - A
{term, default}
usesterm
as the key, if the result does not exist then it usesdefault
as the return/accessor - Just concat as normal or separate with
.
's as necessary because Elixirâs syntax is a bit restrictive. ^.^;
And yes, if you really donât want to use tuple calls (or some annoyances try to remove it) you can pass the lens in at the place of the key
in the Access calls, like ExCore.Access.get(%{a: %{b: 42}}, lens(a.b))
(doesnât need to be inline, you can store and pass it around).
/me is still irritated that people are trying to remove tuple-calls⊠give us some other form of first-class modules if not those then!
Hmm, I should break this new âCoreâ playground into a new thread, I may do that tomorrow. ^.^;
New update, I got curious on if I could make a better for
in Elixir so I whipped something up. It is pretty trivial to add new features to it in a backwards safe way (unlike Elixirâs for
where trying to add a uniq: true
to it is limiting, such a thing is trivial with mine for comparison). However, the benchmark:
defmodule Helpers do
use ExCore.Comprehension
# map * 2
def elixir_0(l) do
for\
x <- l,
do: x * 2
end
def ex_core_0(l) do
comp do
x <- list l
x * 2
end
end
# Into map value to value*2 after adding 1
def elixir_1(l) do
for\
x <- l,
y = x + 1,
into: %{},
do: {x, y * 2}
end
def ex_core_1(l) do
comp do
x <- list l
y = x + 1
{x, y * 2} -> %{}
end
end
end
inputs = %{
"List - 10000 - map*2" => {:lists.seq(0, 10000), &Helpers.elixir_0/1, &Helpers.ex_core_0/1},
"List - 10000 - into map +1 *2" => {:lists.seq(0, 10000), &Helpers.elixir_1/1, &Helpers.ex_core_1/1},
}
actions = %{
"Elixir.for" => fn {input, elx, _core} -> elx.(input) end,
"ExCore.comp" => fn {input, _elx, core} -> core.(input) end,
}
Benchee.run actions, inputs: inputs, time: 5, warmup: 5, print: %{fast_warning: false}
And the results here:
Operating System: Linux
CPU Information: Intel(R) Xeon(R) CPU E5-2620 v3 @ 2.40GHz
Number of Available Cores: 2
Available memory: 8.011072 GB
Elixir 1.6.0-dev
Erlang 20.1
Benchmark suite executing with the following configuration:
warmup: 5.00 s
time: 5.00 s
parallel: 1
inputs: List - 10000 - into map +1 *2, List - 10000 - map*2
Estimated total run time: 40.00 s
Benchmarking with input List - 10000 - into map +1 *2:
Benchmarking Elixir.for...
Benchmarking ExCore.comp...
Benchmarking with input List - 10000 - map*2:
Benchmarking Elixir.for...
Benchmarking ExCore.comp...
##### With input List - 10000 - into map +1 *2 #####
Name ips average deviation median
ExCore.comp 344.12 2.91 ms ±4.00% 2.88 ms
Elixir.for 248.72 4.02 ms ±18.80% 3.75 ms
Comparison:
ExCore.comp 344.12
Elixir.for 248.72 - 1.38x slower
##### With input List - 10000 - map*2 #####
Name ips average deviation median
ExCore.comp 2.64 K 378.83 Όs ±14.71% 402.00 Όs
Elixir.for 2.03 K 493.56 Όs ±11.89% 508.00 Όs
Comparison:
ExCore.comp 2.64 K
Elixir.for 1.03 K - 1.30x slower
Based on knowing what the inputs are (and this is expandable too, whoo!) it can generate better optimized code (which should get even better on larger sets, and there is an obvious optimization here that Iâve not done yet either that could boost it a tiny bit more too). ^.^
Plus it has the boon of not having comma-droppings all over the place! ^.^
Iâm actually not even sure why the pure list mapping version is faster though to be honest, I generate about identical code to elixirâs except I keep the most actively changing bindings last in the stack instead of first (why the frick is elixirâs style to put the most changing bindings first, bleh⊠that is inefficient on the beamâŠ). MehâŠ
Hey all!
Iâm a bit late to this discussion, but Iâd urge you to checkout a library I wrote a little while back, called fun_land, which implements many algebraic data types, including Monads and the components it is built up from.
It also contains rigorous documentation. That said, Monads are such a vague, abstract subject that it is very hard to describe them in a concrete way without for instance simplifying things that are not true for all Monads. I will not claim that my explanation of Monads does not fall under the âMonad Taco tutorial fallacyâ .
PRs welcome, as well as feedback on the documentation and the code.
5 posts were merged into an existing topic: On iteration and enumeration
For a lot more of black magic, check Witchcraft and the rest of their eco-system
https://github.com/expede/witchcraft
Check this podcast
I think before getting straight to monads, there are some other concepts to better know first like monoid, functor âŠ
Another legendary talk, this time from Mister JavaScript in a Pyjama
So, in short the Monad is a concept of value wrapper.
The value wrapper (monad) has a function which accepts the function as an argument and applies it on wrapped value (or each element of wrapped value if it is a list, depends on particular monad implementation and usage).
If it doesnât have a âvalidâ value it should not crash but return some sort of valid result like nil
/ :error
or empty list.
0, 1, or more 'valueâs yes, depending on what the âcontainerâ is. Like :ok
and :error
returns could be a âmonadâ, they can still operate on the no-data
success or error states.
Yep yep.
Eh, no such thing as an âinvalid valueâ for such a thing, the types define what it is. For example the empty list is the âno-valueâ version of a list, or :error
of an {:ok, value}|:error
return as is common in elixir/erlang. These are not invalid values, they are just other âstatesâ of the âmonadâ.