Enumerables and Streams
Enumerables
The concept of enumerables is accessible in Elixir via the Enum
module
There are different kinds of enumerbales in Elixir:
- Lists
- Maps
Enum.map([1, 2, 3], fn x ->
x * 2
end)
[2, 4, 6]
Enum.map(%{1 => 2, 2 => 3, 3 => 4}, fn {k, v} ->
k * v
end)
[2, 6, 12]
Elixir also provides “ranges” which are a useful syntax to describe a range of values you want to iterate over.
Enum.map(1..10, fn x -> x + 1 end)
[2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
Enum.reduce(1..10, 0, fn x, acc ->
acc + x
end)
55
The Enum
module is polymorphic, meaning it can work with many data types while using the same operations. This is achieved by a data structure implementing the Enumerable
protocol
Eager vs Lazy
All functions in the Enum
module are “eager”, meaning they execute immediately
odd? = fn n -> rem(n, 2) != 0 end
#Function<42.3316493/1 in :erl_eval.expr/6>
Enum.filter(1..10, odd?)
[1, 3, 5, 7, 9]
When performing multiple operations with Enum
, each operation will produce an intermediate list until the result is reached
1..100_000
|> Enum.map(fn x -> x * 3 end)
|> Enum.filter(odd?)
|> Enum.sum()
7500000000
The Pipe Operator
Above, Enum
operations are “chained” together with the pipe |>
operator. It takes the output from the left side and passes it as the first argument to the function call on its right side.
It’s purpose is to highlight the data being transformed by a series of functions
Streams
An alternative to the Enum
module is the Stream
module which supports “lazy” operations
1..100_000
|> Stream.map(fn x -> x * 3 end)
|> Stream.filter(odd?)
|> Enum.sum()
7500000000
Breaking down the above pipeline of operations you can better understand how streams work
First the range is passed to Stream.map/2
. In comparison to Enum.map/2
, this will not produce a list but rather a Stream
data structure
1..100_000 |> Stream.map(&(&1 * 3))
#Stream<[enum: 1..100000, funs: [#Function<48.124013645/1 in Stream.map/2>]]>
Streams are composable just like Enums so you can compose many operations together
1..100_000
|> Stream.map(&(&1 * 3))
|> Stream.filter(odd?)
#Stream<[
enum: 1..100000,
funs: [#Function<48.124013645/1 in Stream.map/2>, #Function<40.124013645/1 in Stream.filter/2>]
]>
So instead of producing intermediate lists, Streams build a series of computations that are invoked only when passing the Stream
data structure to the Enum
module. This makes Streams useful when working with large, possibly infinite, collections
1..100_000
|> Stream.map(&(&1 * 3))
|> Stream.filter(odd?)
|> Enum.sum()
7500000000
Many functions in the Stream
module accept an enumerable as an argument and return a Stream
as a result
stream = Stream.cycle(1..3)
stream |> Enum.take(9)
[1, 2, 3, 1, 2, 3, 1, 2, 3]
Stream.unfold/2
can be used to generate values from a given initial value
stream = Stream.unfold("hello", &String.next_codepoint/1)
stream |> Enum.take(3)
["h", "e", "l"]
A notable function is Stream.resource/3
which is used to wrap around resources/operations like reading large files or even slow resources like network resources
"/Users/charlie/github.com/charlieroth/lab/elixir/lorem-ipsum.txt"
|> File.stream!()
|> Stream.flat_map(fn line ->
String.split(line, " ")
end)
|> Stream.map(fn word ->
String.replace(word, "\n", "")
end)
|> Enum.reduce(%{}, fn word, word_count ->
Map.update(word_count, word, 0, fn count ->
count + 1
end)
end)
%{
"at" => 107,
"Maecenas" => 48,
"sit" => 132,
"dui" => 37,
"euismod." => 4,
"dignissim" => 32,
"justo," => 11,
"eget" => 115,
"faucibus," => 1,
"efficitur." => 5,
"convallis," => 6,
"dolor," => 12,
"auctor." => 6,
"odio." => 9,
"non." => 4,
"ad" => 12,
"enim." => 13,
"vehicula," => 1,
"porta." => 3,
"metus." => 15,
"fermentum," => 3,
"eros" => 39,
"luctus" => 36,
"nibh," => 13,
"eget," => 9,
"accumsan" => 38,
"turpis" => 31,
"id" => 113,
"tellus," => 7,
"consequat." => 5,
"dictum." => 3,
"felis" => 40,
"et," => 11,
"nec." => 4,
"est." => 16,
"nunc" => 31,
"posuere." => 5,
"mauris" => 34,
"gravida" => 30,
"ultricies," => 2,
"pulvinar" => 39,
"felis." => 12,
"pharetra" => 35,
"placerat." => 11,
"tempus," => 4,
"purus" => 43,
"Interdum" => 3,
"sagittis," => 4,
"neque" => 40,
"imperdiet." => 8,
...
}