iamafanasyev
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)
Trending in Announcing
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixirconf-us
- #ai
- #blog-post
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #hex
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
hst337
I didn’t get what are the features and why can’t I just use
Streamfor every lazily evaluated enumerable?All of your examples can be rewritten using
Stream(orEnumerablein case of two streams’ product)hst337
And this tuple syntax is unique across all Elixir’s macros and will be confusing for elixir developers. I suggest nested structure to imitate variadic function like
Here you just need to implement the
bindable/2macro and parse variadicforcall as an ASThst337
But my last suggestion would be to implement
Stream.productfunction in the PR to the elixir-lang instead of creating custom macro with unfamiliar (to elixir devs) syntax.This would solve the problem with
for-style lazy evaluation as a product of two enumerables and it would be compatible with both elixir language design andStreamAPIAnyway, great work!
iamafanasyev
Thanks for the feedback!
That is not about
Streamsat all. As I mentioned,Streamcomposition comes as a bonus.Anyway, let’s talk
Streams: Elixir’sforis a great facility to create a new lists (well, up to:reducefeature, using which you can obtain any value). And one of the great option of it is “contextual” filtration. And the thing is:can not be reproduced in general with
Stream.product, as it works on independent streams:or if you have only two streams combinator:
So I don’t think
Stream.productwould close the gap as aStreamcombinator (at the end of the day,foris a way more readable option to construct new values).iamafanasyev
The main point:
forfacility not only for lists.If you have a data type, that “behaves flat-mappable”, you can use it inside
for.iamafanasyev
Clever trick, I like it!
(but we still need to use parentheses to explicitly bound the only
bindableargument)hst337
What about
?
That’s mostly because you’re comping from Scala. Elixir developers use Streams for lazy collections all the time.
foris not for lists only. It accepts any enumerable, but produces anyCollectableyou’re specifiying in optioninto. While yourforsolution always produces a composition of something your custom protocols implement. I undestand that this is scala-like approach, and it looks really interesting to play with, and I definitely have nothing against this approach, but I am just noticing thatStreamin ElixirAnd with this in mind, your ideas will just be alien to the ecosystem, since they do not match regular development style. You can take a look at
witchcraftandalgaeprojects which brought algebraic typing ideas to Elixir. These libraries became a fun tools to play with, not a widely adopted practice for the community and the ecosystem.Any implementation of algebraic types in Elixir comes across a well-know set of problems like
While your library certainly gives an unusual (for Elixir ecosystem) approach to solve these problems with lazy evaluation and it is interesting to talk about, I can only suggest you to get a grasp of Elixir’s approach to these problems in return.
For example, for resource management in Elixir we usually use something like this
This kind of approach is more fault tolerant because
It handles infinite loops during resource acquisition. If the function (
func) using resource is taking more than 5 seconds, it will be forcibly terminatedIt handles stack overflow and any other memory exhaustion issue, because as soon the working process hits the memory limit, it will be killed by the VM (or VM will crash, it’s configurable) and the supervising process will close the resource
It handles hardware outages (sic!!!), because we can spawn the working process on the separate node, and when the separate node has hardware outage, we will receive either exit signal or a timeout in receive
For more on resource management, I’d suggest reading libraries
NimblePoolandDBConnectionsince they’re doing exactly what I’ve described above.iamafanasyev
Really appreciate such a deep and comprehensive reply. And I’m inlined with almost all of it. I’ll try to make a more refined statement to reason about:
There is nothing about lazy evaluation of anything in the library, as well as adopting some external abstraction in the first place (nor addressing performance aspects). It’s only DX. Initially I was addressing the problem of copy-pasting and refactoring the same code-approaches across our projects (e.g. time bounded synchronous expression evaluation or “singleton-resource” management). Stick with Elixir’s kernel (primary building blocks) as much as possible was a must. As a result I ended up poking around:
Stream.resource/3, which is (from the-day-one) a way to “handle resources” of the “stream-nature” (so it’s like 2-in-1: Elixir’s standard library resource management + acquired resource content streaming);foras a “composition facility” for enumerables (as I mentioned before, I started with some kind ofResource.use_all!/2, until I realised,foris a more natural candidate as it does exactly the same for lists as a syntactic sugar).And all this “composable hustle” was just to refactor these patterns into separate library, so they target one concept at a time in a general way, so no preconditions nor assumptions on the nature of resources was made (should it be a file, database connection pool or whatever).
And that is where our points of view diverge:
Stream.resource/3as a stream in the first place, but with “resource management” baked in;at_resource-approach or adoptingStream.resource/3);foras an exclusive facility for enumerables;foralso accepts streams (when you really need aStreamas a result).So these libraries are nothing more than to view these points as a some sort of asymmetry:
Stream.resource/3) without “stream-nature” assumption on the acquired resource content (which could be implemented usingat_resource-approach);foras a for-comprehension (just as its name suggestsKernel.SpecialForms.for/1).mudasobwa
FWIW, with LazyFor v1.1.0 — Documentation one might do
mudasobwa
Kernel.SpecialForms.for/1is translated to erlang comprehension not toflat_map/2+filter/2.