Bindable - General purpose for-comprehension at your service

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);
  • for as a “composition facility” for enumerables (as I mentioned before, I started with some kind of Resource.use_all!/2, until I realised, for is 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:

  • you can view Stream.resource/3 as a stream in the first place, but with “resource management” baked in;
  • being able to:
    Stream.resource(
      fn -> File.open!("sample") end,
      fn file ->
        case IO.read(file, :line) do
          data when is_binary(data) -> {[data], file}
          _ -> {:halt, file}
        end
      end,
      fn file -> File.close(file) end
    )
    
    but do-it-by-yourself for resources of “non-stream-nature” every time you need it (no matter how: using at_resource-approach or adopting Stream.resource/3);
  • you can view for as an exclusive facility for enumerables;
  • and at the same time being able to:
    for a <- as, b <- bs, a < b, c <- cs, a + b + c < 10, do: a + b + c
    
    for lists, but:
    Stream.product(as, bs)
    |> Stream.filter(fn {a, b} -> a < b end)
    |> Stream.product(c)
    |> Stream.fitler(fn {{a, b}, c} -> a + b + c < 10 end)
    |> Stream.map(fn {{a, b}, c} -> a + b + c end)
    
    for streams, although for also accepts streams (when you really need a Stream as a result).

So these libraries are nothing more than to view these points as a some sort of asymmetry:

  • being able to use Elixir’s kernel “resource management” facility (Stream.resource/3) without “stream-nature” assumption on the acquired resource content (which could be implemented using at_resource-approach);
  • being able to use for as a for-comprehension (just as its name suggests :slight_smile:, I’m totally ok with Kernel.SpecialForms.for/1).