I’ll try provide an additional angle
To me, Stream.resource/3 is a merge between the bracket pattern applied to streams, and unfold along flat_map.
What the bracket pattern entails, is a way to manage resource:
- how to acquire it
- how to release it
It’s really simple, but it gives you this guarantee: If the resource acquisition is successful, then it will be released no matter what happen within the computation. (:kill signals tend to be external in origin
)
And as a basic function, it would be just defined as:
def bracket(acquire, release, compute) do
resource = acquire.()
try do
compute.(resource)
after
release.(resource)
end
end
Where would you want to use this pattern ?
We can look at Python’s with statement as a first example. It is commonly used to handle files.
acquire = fn -> File.open!("some.log") end
release = &File.close/1
bracket(acquire, release, fn handle ->
# Do your thing
end)
We can do the same for using a temporary ets table:
acquire = fn -> :ets.new(nil, []) end
release = &:ets.delete/1
bracket(acquire, release, fn handle ->
# Do your other thing
end)
Etc…
But what if we wanted to do particular manipulation with our resource handle and return a stream as a result of that ?
stream = bracket(acquire, release, fn handle ->
complex_logic_that_returns_a_stream(handle)
end)
manipulate_stream(stream)
This won’t work, because the resource will be released before we use it. We could move the rest of the code inside the compute fun. But what if we want to reuse the stream for a different set of manipulation ? We need to redo the bracket. And that’s a departure from the what a stream can usually do.
To restore that, we need to define a bracket function that’s a bit more complex:
def bracket_stream(acquire, release, compute) do
next_fun = fn
{:stop, handle} -> {:halt, handle}
handle -> {complex_logic_that_returns_a_stream(handle), {:stop, handle}}
end
Stream.resource(acquire, next_fun, release)
end
Now our stream will work and we only need to care about the resource management in one place.
So what about Stream.resource/3 itself ? We could have a Stream.bracket/3 defined by itself in the standard library that wouldn’t reuse Stream.resource/3. And within we could use Stream.unfold/2 with the resource handle, and if our domain logic forces chunks upon us we can simply compose the stream with Stream.concat/1.
I think this was probably the most common use case, and only Stream.resource/3 was implemented to represent that case.
If we return to the bracket pattern, It’s kind of tedious to always reuse the same code for opening and closing files, or creating and destroying ets tables, or whatever else. It can be abstracted further to only provide the config used by the acquire and release code, along the computation fun. For example, for files we can imagine the following function:
def with_handle(path, compute) do
acquire = fn -> File.open!(path) end
release = &File.close/1
bracket(acquire, release, compute)
end
Then we could just do
File.with_handle("some.log", &analyse_logs/1)
And that something we already have in the standard library 
File.open("some.log", &analyse_logs/1)
About Stream.transform/3,4,5, at work we mainly use it as a substitute for Task.async_stream/2,3. At first because async_stream didn’t exist yet, then because we noticed it was way better in term of performance in some cases. But rereading the code, it’s just behaving the same as a convoluted Stream.resource/3. (By that I mean that if we had Stream.bracket/3 the code would be simpler.)
We do use the non resource variant to do BSON stream decoding. This is doing a stateful map on a file stream.
And that’s pretty much how I see Stream.transform/3,4,5: modifying a stream with a stateful flat_map, possibly bringing along resource management.