Loop

An Elixir macro that provides imperative-style loop syntax with automatic compile-time optimization to functional patterns. Write loops like you would in imperative languages, and let the compiler intelligently transform them to functional patterns.

During an interview with Prime, José Valim discussed a common challenge faced by new programmers: understanding complex functional patterns such as map-reduce. In addition to these patterns, others like simple reducers and recursion can be equally daunting for beginners. My proof-of-concept application addresses this issue by enabling inexperienced developers to write imperative-style loops that are familiar to them, while still allowing them to learn the underlying idiomatic functional constructs.

Features

Quick Start

use Loop

# Basic infinite loop (broken with `break/0` or `break/1`)
loop do
  IO.puts("repeating...")
  Process.sleep(500)
end

# Loop with state
i = 0
loop do
  IO.puts(i)
  i = i + 1
  if i == 10, do: break()
end

# Using initial binding
loop count: 0 do
  IO.puts(count)
  if count >= 5, do: break(count)
  count = count + 1
end

Loop recognizes common patterns and rewrites them internally

quote do
  loop product: 1 do
    if list == [], do: break(product)
    product = product * hd(list)
    list = tl(list)
  end
end
|> Macro.expand(__ENV__)
|> Macro.to_string()        #=> "Enum.product(list)"

Core Concepts

Breaking Out of Loops

Use break() to exit a loop with nil, or break(value) to exit with a specific value:

loop do
  break(123)  # Returns 123
end

State Across Iterations

Bindings at the end of each iteration are carried to the next, creating the illusion of mutable state:

i = 0
loop do
  IO.puts(i)
  i = i + 1 # This binding carries to the next iteration
end

Initial Bindings

Declare initial values using keyword arguments:

loop i: 0, step: 2 do
  IO.puts(i)
  i = i + step
end

Automatic Pattern Optimization

Loop recognizes many common patterns and automatically optimizes them to equivalent Enum operations at compile-time, with zero runtime overhead.

The 26 classic examples are below, and there are additional advanced patterns too.

1. Map

loop acc: [] do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  acc = [h * h | acc]
end
# => Enum.map(list, fn h -> h * h end)

2. Filter

loop acc: [] do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  acc = if rem(h, 2) == 0, do: [h | acc], else: acc
end
# => Enum.filter(list, fn h -> rem(h, 2) == 0 end)

3. Reject

loop acc: [] do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  acc = if rem(h, 2) == 0, do: acc, else: [h | acc]
end
# => Enum.reject(list, fn h -> rem(h, 2) == 0 end)

4. Reverse

loop acc: [] do
  if list == [], do: break(acc)
  [h | list] = list
  acc = [h | acc]
end
# => Enum.reverse(list)

5. Filter+Map

loop acc: [] do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  acc = if rem(h, 2) == 0, do: [h * 10 | acc], else: acc
end
# => for h <- list, rem(h, 2) == 0, do: h * 10

6. Find

loop do
  if list == [], do: break(nil)
  [h | list] = list
  if String.starts_with?(h, "c"), do: break(h)
end
# => Enum.find(list, fn h -> String.starts_with?(h, "c") end)

7. Member?

loop do
  if list == [], do: break(false)
  [h | list] = list
  if h == target, do: break(true)
end
# => Enum.member?(list, target)

8. Find Index

loop index: 0 do
  if list == [], do: break(nil)
  [h | list] = list
  if rem(h, 2) == 0, do: break(index)
  index = index + 1
end
# => Enum.find_index(list, fn h -> rem(h, 2) == 0 end)

9. Count

loop count: 0 do
  if list == [], do: break(count)
  [h | list] = list
  count = if h > 5, do: count + 1, else: count
end
# => Enum.count(list, fn h -> h > 5 end)

10. Length

loop count: 0 do
  if list == [], do: break(count)
  [_ | list] = list
  count = count + 1
end
# => length(list)

11. Any

loop result: false do
  if list == [], do: break(result)
  [h | list] = list
  result = result or rem(h, 2) == 0
end
# => Enum.any?(list, fn h -> rem(h, 2) == 0 end)

12. All

loop result: true do
  if list == [], do: break(result)
  [h | list] = list
  result = result and rem(h, 2) == 0
end
# => Enum.all?(list, fn h -> rem(h, 2) == 0 end)

13. Each

loop do
  if list == [], do: break()
  [h | list] = list
  IO.puts(h)
end
# => Enum.each(list, fn h -> IO.puts(h) end)

14. Take While

loop acc: [] do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  acc = if h > 0, do: [h | acc], else: break(Enum.reverse(acc))
end
# => Enum.take_while(list, fn h -> h > 0 end)

15. Drop While

loop do
  if list == [], do: break([])
  [h | list] = list
  unless h < 3, do: break([h | list])
end
# => Enum.drop_while(list, fn h -> h < 3 end)

16. With Index

loop acc: [], i: 0 do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  acc = [{h, i} | acc]
  i = i + 1
end
# => Enum.with_index(list)

loop acc: [], i: 5 do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  acc = [{h, i} | acc]
  i = i + 1
end
# => Enum.with_index(list, 5)

17. Zip

loop acc: [] do
  if list1 == [] or list2 == [], do: break(Enum.reverse(acc))
  [h1 | list1] = list1
  [h2 | list2] = list2
  acc = [{h1, h2} | acc]
end
# => Enum.zip(list1, list2)

18. Reduce While

loop acc: 0 do
  if list == [], do: break(acc)
  [h | list] = list
  if acc + h > 6, do: break(acc)
  acc = acc + h
end
# => Enum.reduce_while(list, 0, fn h, acc -> ... end)

19. Dedup

loop acc: [], prev: nil do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  acc = if h == prev, do: acc, else: [h | acc]
  prev = h
end
# => Enum.dedup(list)

20. Max

loop best: hd(list) do
  list = tl(list)
  if list == [], do: break(best)
  best = max(best, hd(list))
end
# => Enum.max(list)

21. Min

loop best: hd(list) do
  list = tl(list)
  if list == [], do: break(best)
  best = min(best, hd(list))
end
# => Enum.min(list)

22. Frequencies

loop freq: %{} do
  if list == [], do: break(freq)
  [h | list] = list
  freq = Map.update(freq, h, 1, &(&1 + 1))
end
# => Enum.frequencies(list)

23. Map.new

loop acc: %{} do
  if list == [], do: break(acc)
  [h | list] = list
  acc = Map.put(acc, elem(h, 0), elem(h, 1))
end
# => Map.new(list, fn h -> {elem(h, 0), elem(h, 1)} end)

24. Scan

loop acc: [], running: 0 do
  if Enum.empty?(list), do: break(Enum.reverse(acc))
  [h | list] = list
  running = running + h
  acc = [running | acc]
end
# => Enum.scan(list, 0, fn x, running -> running + x end)

25. Sum

loop sum: 0 do
  if list == [], do: break(sum)
  sum = sum + hd(list)
  list = tl(list)
end
# => Enum.sum(list)

26. Product / Reduce

loop product: 1 do
  if list == [], do: break(product)
  product = product * hd(list)
  list = tl(list)
end
# => Enum.product(list)
# (Other init/op combos become Enum.reduce)

Additional Advanced Patterns (Showcase)

Flat Map

loop acc: [] do
  if list == [], do: break(acc)
  [h | list] = list
  acc = acc ++ [h, -h]
end
# => Enum.flat_map(list, fn h -> [h, -h] end)

Map Reduce

loop mapped: [], state: 0 do
  if list == [], do: break({Enum.reverse(mapped), state})
  [h | list] = list
  mapped = [h + state | mapped]
  state = state + h
end
# => Enum.map_reduce(list, 0, fn h, state -> {h + state, state + h} end)

Group By

loop groups: %{} do
  if list == [], do: break(groups)
  [h | list] = list
  key = rem(h, 2)
  groups = Map.update(groups, key, [h], &(&1 ++ [h]))
end
# => Enum.group_by(list, &rem(&1, 2))

Uniq By

loop acc: [], seen: MapSet.new() do
  if list == [], do: break(Enum.reverse(acc))
  [h | list] = list
  key = rem(h, 3)
  acc = if MapSet.member?(seen, key), do: acc, else: [h | acc]
  seen = MapSet.put(seen, key)
end
# => Enum.uniq_by(list, &rem(&1, 3))

Chunk Every

loop chunks: [] do
  if list == [], do: break(Enum.reverse(chunks))
  chunks = [Enum.take(list, size) | chunks]
  list = Enum.drop(list, size)
end
# => Enum.chunk_every(list, size)

loop chunks: [] do
  if list == [], do: break(Enum.reverse(chunks))
  chunks = [Enum.take(list, size) | chunks]
  list = Enum.drop(list, step)
end
# => Enum.chunk_every(list, size, step)

loop chunks: [] do
  if list == [] or length(list) < size, do: break(Enum.reverse(chunks))
  chunks = [Enum.take(list, size) | chunks]
  list = Enum.drop(list, step)
end
# => Enum.chunk_every(list, size, step, :discard)

Split While

loop left: [] do
  if list == [], do: break({Enum.reverse(left), []})
  [h | list] = list
  left = if h > 0, do: [h | left], else: break({Enum.reverse(left), [h | list]})
end
# => Enum.split_while(list, &(&1 > 0))

Zip With / Unzip

loop acc: [] do
  if list1 == [] or list2 == [], do: break(Enum.reverse(acc))
  [x | list1] = list1
  [y | list2] = list2
  acc = [x + y * 2 | acc]
end
# => Enum.zip_with(list1, list2, fn x, y -> x + y * 2 end)

loop left: [], right: [] do
  if list == [], do: break({Enum.reverse(left), Enum.reverse(right)})
  [pair | list] = list
  left = [elem(pair, 0) | left]
  right = [elem(pair, 1) | right]
end
# => Enum.unzip(list)

Max By / Min By / Frequencies By

loop best: hd(list), best_key: String.length(hd(list)) do
  list = tl(list)
  if list == [], do: break(best)
  candidate = hd(list)
  candidate_key = String.length(candidate)
  {best, best_key} = if candidate_key > best_key, do: {candidate, candidate_key}, else: {best, best_key}
end
# => Enum.max_by(list, &String.length/1)
# (same shape with < => Enum.min_by/2)

loop freq: %{} do
  if list == [], do: break(freq)
  [h | list] = list
  freq = Map.update(freq, String.first(h), 1, &(&1 + 1))
end
# => Enum.frequencies_by(list, &String.first/1)

Practical Examples

Counter Service

Spawn a counter process with a message loop:

pid = spawn_link(fn ->
  loop counter: 0 do
    counter =
      receive do
        :inc -> counter + 1
        :dec -> counter - 1
        {:get, from} -> send(from, counter); counter
        :stop -> break()
      end
  end
end)

send(pid, :inc)
send(pid, :inc)
send(pid, {:get, self()})
flush()  # => 2
send(pid, :stop)

Random Pattern Animation

loop do
  IO.write(Enum.random(["░", "▒", "▓", "█"]))
  Process.sleep(100)
end

Important Notes

Design Philosophy

Loop is a thought experiment and proof of concept showing that:

  1. Elixir's meta-programming capabilities enable unconventional syntax
  2. Macros can recognize algorithmic patterns
  3. Imperative-looking code can be compiled to efficient functional operations