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

Advent of Code - Day 14

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
end

Structural 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
end

Functional 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 more

Godfather

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)
end

Homework

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()
end

Just 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