Difference between `Stream.each/2` and `Stream.map/2`?

Viewed 67

They have the same (or equivalent) typespecs:

each(Enumerable.t(), (element() -> term())) :: Enumerable.t()
map(Enumerable.t(), (element() -> any())) :: Enumerable.t()

The types term and any are equivalent per the docs:

The docs don't point out any obvious differences, beyond the intended/expected usage (each/2 mentions side effects):

For simple examples they seem equivalent:

iex(5)> Stream.each( [1, 2, 3], &(&1) ) |> Enum.to_list()
[1, 2, 3]
iex(8)> Stream.map( [1, 2, 3], &(&1) ) |> Enum.to_list() 
[1, 2, 3]

The example in the docs for each/2 also seems to work fine with map/2:

iex(9)> stream = Stream.map([1, 2, 3], fn x -> send(self(), x) end)
#Stream<[enum: [1, 2, 3], funs: [#Function<48.50989570/1 in Stream.map/2>]]>
iex(10)> Enum.to_list(stream)
[1, 2, 3]
iex(11)> receive do: (x when is_integer(x) -> x)
1
iex(12)> receive do: (x when is_integer(x) -> x)
2
iex(13)> receive do: (x when is_integer(x) -> x)
3

If Stream.each/2 and Stream.map/2 ARE different – which I'm guessing they are – what is a simple example demonstrating those differences (that could be easily run in iex)? Do they differ in less obvious (e.g. harder to demonstrate) ways as well?

2 Answers

Stream.map/2 will transform the enumerable: it RETURNS the result of its operation. Stream.each/2 does not modify the enumerable; it's useful for emitting side-effects only.

iex> Stream.map([1, 2, 3], fn x -> x * 2 end) |> Enum.to_list()
[2, 4, 6]
iex> Stream.each([1, 2, 3], fn x -> x * 2 end) |> Enum.to_list()
[1, 2, 3]

what is a simple example demonstrating those differences (that could be easily run in iex)?

Here's a stream that squares a number then converts to strings:

iex> 1..10 |> Stream.map(&(&1*&1)) |> Stream.map(&Integer.to_string/1) |> Enum.to_list()
["1", "4", "9", "16", "25", "36", "49", "64", "81", "100"]

You can see that the transformed output of the first map was used in the 2nd map.

If we however switch either of those maps to each, the stream is not transformed and the stream retains its previous output:

Not squared, but converted to strings:

iex> 1..10 |> Stream.each(&(&1*&1)) |> Stream.map(&Integer.to_string/1) |> Enum.to_list()
["1", "2", "3", "4", "5", "6", "7", "8", "9", "10"]

Squared, but not converted to string:

iex> 1..10 |> Stream.map(&(&1*&1)) |> Stream.each(&Integer.to_string/1) |> Enum.to_list()
[1, 4, 9, 16, 25, 36, 49, 64, 81, 100]

Do they differ in less obvious (e.g. harder to demonstrate) ways as well?

No, but using each without generating a side-effect is not the intended use-case. The real benefit of each is to do things like inspect the current value between stages:

iex> inspect_even = fn x -> IO.puts("#{x} is #{if rem(x, 2) == 0, do: "even", else: "odd"}") end
iex> 1..3 |> Stream.each(inspect_even) |> Stream.map(&(&1 * 2)) |> Stream.each(inspect_even) |> Enum.to_list
1 is odd
2 is even
2 is even
4 is even
3 is odd
6 is even
[2, 4, 6]
Related