README.md

# 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](https://youtu.be/-mFJ5rPbY_w?t=2388), 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

- **Imperative Loop Syntax** - Write familiar `loop`/`break` constructs
- **Automatic Optimization** - Recognizes common patterns and optimizes to `Enum` functions
- **Mutable-like State** - Bindings carry over between iterations, simulating mutable state
- **Pattern Recognition** - Supports dozens of optimization patterns, including advanced collection transforms

## Quick Start

```elixir
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

```elixir
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:

```elixir
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:

```elixir
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:

```elixir
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

```elixir
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

```elixir
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

```elixir
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

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

### 5. Filter+Map

```elixir
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

```elixir
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?

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

### 8. Find Index

```elixir
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

```elixir
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

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

### 11. Any

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

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

### 21. Min

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

### 22. Frequencies

```elixir
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

```elixir
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

```elixir
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

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

### 26. Product / Reduce

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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

```elixir
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:

```elixir
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

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

## Important Notes

- **Scoping**: Loops don't modify surrounding scope. Capture the return value:

  ```elixir
  a = 10
  result =
    loop do
      a = a - 1 # doesn't affect outer a
      if a < 2, do: break({:final, a})
    end
  # a is still 10 here
  ```

- **Proof of Concept**: This library demonstrates that Elixir macros are
  powerful enough to bridge imperative and functional paradigms. However,
  idiomatic Elixir code typically uses `Enum` functions directly. I'm not
  suggesting you should code with `loop`s in Elixir.

## 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