Stream.unfold
Working with Elixir?
You will be bored
Working with FP?
Probably you will be bored
Never worked with FP?
It might be rocket science
But fear not, it's not that complicated
Who I am?
- I work with Ruby for ~7 years
- I had FP on studies more than 10 years ago
- Never worked with FP
- Monads still confuse me
Inspiration

Initial attempt

Stream.unfold for the rescue

Let's start simple
def example(foo)
array = []
for i in 1..10
array[i - 1] = i * foo
# or
array = suprise_me(array, i, foo)
end
array
endStructural approach
def example(foo)
(1..10).map do |i|
i * foo
# or
function_call(i, foo)
# can't happen
suprise_me(array, i, foo)
end
endFunctional approach
- no mutations
- no side effects
- declarative
- more readable, but
- requires a different way of thinking
Family of functions

Family of functions
enum.map { |i| i * 2 }
enum.select { |i| i % 2 == 0 }
enum.reject { |i| i % 2 == 0 }
enum.take(5)
enum.sort { |x, y| x <=> y }
enum.all? { |i| i % 2 == 0 }
enum.any? { |i| i % 2 == 0 }
# and moreGodfather

Godfather
- fold
- reduce
- accumulate
- aggregate
- compress
- inject
Map with reduce
# Ruby
def map(collection, &fun)
collection.inject([]) do |acc, elem|
acc + [fun.call(elem)]
end
end
# Elixir
def map(collection, fun) do
collection
|> Enum.reduce([], fn elem, acc ->
acc ++ [fun.(elem)]
end)
endHomework
Implement Enum functions with reduce
Time for fun part - Laziness

Stream
A lazy version of Enum module
1..1_000_000
|> Stream.map(fn x -> x * 2 end)
|> Stream.filter(fn x -> rem(x, 2) == 0 end)
|> Enum.take(5)Stream.iterate/2
Stream.unfold/2
Think: "values generator"
start_value
|> Stream.iterate(fn current_value ->
next_value
end)Think: "reduce reverse"
{0, 1}
|> Stream.unfold(fn {current, next} ->
{
current,
{
next,
current + next
}
}
end)
# {0, {1, 1}}
# {1, {1, 2}}
# {1, {2, 3}}
# {2, {3, 5}}
# {3, {5, 8}}
Back to AoC
Back to AoC
{elf1, elf2} = {0, 1}
scores = :array.new()
scores = :array.set(elf1, 3, scores)
scores = :array.set(elf2, 7, scores)
{scores, elf1, elf2, 0}
|> Stream.unfold(fn {scores, elf1, elf2, index} ->
elf1_score = :array.get(elf1, scores)
elf2_score = :array.get(elf2, scores)
scores =
combine_recipes(elf1_score, elf2_score)
|> Enum.reduce(scores, fn recipe, scores ->
:array.set(:array.size(scores), recipe, scores)
end)
elf1 = Integer.mod(elf1 + elf1_score + 1, :array.size(scores))
elf2 = Integer.mod(elf2 + elf2_score + 1, :array.size(scores))
{:array.get(index, scores), {scores, elf1, elf2, index + 1}}
end)
|> Stream.drop(681_901)
|> Enum.take(10)When organised
{scores, elf1, elf2} = init()
scores
|> unfold_stream(elf1, elf2)
|> Stream.drop(puzzle_input)
|> Enum.take(10)Initialization
# positions
{elf1, elf2} = {0, 1}
# initial array of scores
scores = :array.new()
scores = :array.set(elf1, 3, scores)
scores = :array.set(elf2, 7, scores)
# result
{scores, elf1, elf2}Helper
def combine_recipes(left, right) do
(left + right) |> Integer.digits()
endJust the meat
Stream.unfold(fn {scores, elf1, elf2, index} ->
# current elves' scores
elf1_score = :array.get(elf1, scores)
elf2_score = :array.get(elf2, scores)
# append new recipe(s)
scores =
combine_recipes(elf1_score, elf2_score)
|> Enum.reduce(scores, fn recipe, scores ->
:array.set(:array.size(scores), recipe, scores)
end)
# move elves
elf1 = Integer.mod(elf1 + elf1_score + 1, :array.size(scores))
elf2 = Integer.mod(elf2 + elf2_score + 1, :array.size(scores))
# return value for CURRENT index and new accumulator
{:array.get(index, scores), {scores, elf1, elf2, index + 1}}
end)Usage
{scores, elf1, elf2} = init()
scores
|> unfold_stream(elf1, elf2)
|> Stream.drop(puzzle_input)
|> Enum.take(10)Conclusion
- functional means declarative
- when your code becomes huge and imperative - stop and re-think

We're hiring!

source: https://elixir.career/
Stream.unfold
By Jan Dudulski
Stream.unfold
- 156