While playing around with public API of Resource I stuck with composability problem (also got a topic on it). I was wondering what should public API look like to expose composable resources (to work with several resources while the library handles all the safe acquire/release hustle).
# Can we do better?
Resource.use!(ra, fn a ->
Resource.use!(rb, fn b ->
Resource.use!(rc, fn c ->
f(a, b, c)
end)
end)
end)
There is already syntactic sugar to tackle such problem in a functional programming world — for-comprehension (or e.g. its cousin do-notation). It allows you to write effectful code in an imperative style. Elixir already has for-comprehension:
for x <- [1, 2, 3], x < 3,
y <- [4, 5, 6], y > 4 do
{x, y}
end
# [{1, 5}, {1, 6}, {2, 5}, {2, 6}]
Which in fact is just a:
Enum.flat_map(Enum.filter([1, 2, 3], fn x -> x < 3 end), fn x ->
Enum.flat_map(Enum.filter([4, 5, 6], fn y -> y > 4 end), fn y ->
[{x, y}]
end)
end)
# [{1, 5}, {1, 6}, {2, 5}, {2, 6}]
However, it works only for lists (and that’s fine, it has list-specific functionality, e.g. :uniq option, or :reduce option).
So the library exposes “general purpose” for-comprehension. It work with any kind of “monadic container” (and doesn’t have a specific options to any particular one):
require Bindable.ForComprehension
Bindable.ForComprehension.for {
x <- [1, 2, 3], if(x < 3),
y <- [4, 5, 6], if(y > 4)
} do
{x, y}
end
# [{1, 5}, {1, 6}, {2, 5}, {2, 6}]
Due to the fact, that under the hood it is nothing more (up to some details) than just a flat_map(m(a), (a -> m(b))) :: m(b)
you always end up with “containered-value”. In other words, for-comprehension is a way to construct a new (monadic) value.
That’s why as a nice bonus you also get a facility to lazily(!) construct a new streams (Elixir’s for eagerly evaluates enumerables, as it uses Enum.flat_map/2 under the hood):
Bindable.ForComprehension.for {
x <- Stream.map(1..5, fn x -> if(x > 2, do: (raise "boom"), else: x) end),
y <- Stream.map(1..5, fn y -> if(y > 2, do: (raise "boom"), else: y) end)
} do
{x, y}
end |> Enum.take(2)
# [{1, 1}, {1, 2}]
And finally:
Bindable.ForComprehension.for {
a <- ra,
b <- rb,
c <- rc
} do
f(a, b, c)
end
“Scala-like” implementation was selected intentionally for two main reasons:
- it is already known and well established syntax;
- it solves a “variadic-problem” (from compiler’s perspective we define macro accepting two arguments: a tuple and a “do-block”).
More notable implementation insides could be found on Bindable (hex)
Bindable (GitHub)