|
| 1 | +defmodule PipeOperator do |
| 2 | + @moduledoc false |
| 3 | + use Koans |
| 4 | + |
| 5 | + @intro "The Pipe Operator - Making data transformation elegant and readable" |
| 6 | + |
| 7 | + koan "The pipe operator passes the result of one function to the next" do |
| 8 | + result = |
| 9 | + "hello world" |
| 10 | + |> String.upcase() |
| 11 | + |> String.split(" ") |
| 12 | + |> Enum.join("-") |
| 13 | + |
| 14 | + assert result == ___ |
| 15 | + end |
| 16 | + |
| 17 | + koan "Without pipes, nested function calls can be hard to read" do |
| 18 | + nested_result = Enum.join(String.split(String.downcase("Hello World"), " "), "_") |
| 19 | + piped_result = "Hello World" |> String.downcase() |> String.split(" ") |> Enum.join("_") |
| 20 | + |
| 21 | + assert nested_result == piped_result |
| 22 | + assert piped_result == ___ |
| 23 | + end |
| 24 | + |
| 25 | + koan "Pipes pass the result as the first argument to the next function" do |
| 26 | + result = |
| 27 | + [1, 2, 3, 4, 5] |
| 28 | + |> Enum.filter(&(&1 > 2)) |
| 29 | + |> Enum.map(&(&1 * 2)) |
| 30 | + |
| 31 | + assert result == ___ |
| 32 | + end |
| 33 | + |
| 34 | + koan "Additional arguments can be passed to piped functions" do |
| 35 | + result = |
| 36 | + "hello world" |
| 37 | + |> String.split(" ") |
| 38 | + |> Enum.join(", ") |
| 39 | + |
| 40 | + assert result == ___ |
| 41 | + end |
| 42 | + |
| 43 | + koan "Pipes work with anonymous functions too" do |
| 44 | + double = fn x -> x * 2 end |
| 45 | + add_ten = fn x -> x + 10 end |
| 46 | + |
| 47 | + result = |
| 48 | + 5 |
| 49 | + |> double.() |
| 50 | + |> add_ten.() |
| 51 | + |
| 52 | + assert result == ___ |
| 53 | + end |
| 54 | + |
| 55 | + koan "You can pipe into function captures" do |
| 56 | + result = |
| 57 | + [1, 2, 3] |
| 58 | + |> Enum.map(&Integer.to_string/1) |
| 59 | + |> Enum.join("-") |
| 60 | + |
| 61 | + assert result == ___ |
| 62 | + end |
| 63 | + |
| 64 | + koan "Complex data transformations become readable with pipes" do |
| 65 | + users = [ |
| 66 | + %{name: "Bob", age: 25, active: false}, |
| 67 | + %{name: "Charlie", age: 35, active: true}, |
| 68 | + %{name: "Alice", age: 30, active: true} |
| 69 | + ] |
| 70 | + |
| 71 | + active_names = |
| 72 | + users |
| 73 | + |> Enum.filter(& &1.active) |
| 74 | + |> Enum.map(& &1.name) |
| 75 | + |> Enum.sort() |
| 76 | + |
| 77 | + assert active_names == ___ |
| 78 | + end |
| 79 | + |
| 80 | + koan "Pipes can be split across multiple lines for readability" do |
| 81 | + result = |
| 82 | + "the quick brown fox jumps over the lazy dog" |
| 83 | + |> String.split(" ") |
| 84 | + |> Enum.filter(&(String.length(&1) > 3)) |
| 85 | + |> Enum.map(&String.upcase/1) |
| 86 | + |> Enum.take(3) |
| 87 | + |
| 88 | + assert result == ___ |
| 89 | + end |
| 90 | + |
| 91 | + # TODO: Fix this example. It doesn't illustrate the point well. |
| 92 | + koan "The then/2 function is useful when you need to call a function that doesn't take the piped value as first argument" do |
| 93 | + result = |
| 94 | + [1, 2, 3] |
| 95 | + |> Enum.map(&(&1 * 2)) |
| 96 | + |> then(&Enum.zip([:a, :b, :c], &1)) |
| 97 | + |
| 98 | + assert result == ___ |
| 99 | + end |
| 100 | + |
| 101 | + koan "Pipes can be used with case statements" do |
| 102 | + process_number = fn x -> |
| 103 | + x |
| 104 | + |> Integer.parse() |
| 105 | + |> case do |
| 106 | + {num, ""} -> {:ok, num * 2} |
| 107 | + _ -> {:error, :invalid_number} |
| 108 | + end |
| 109 | + end |
| 110 | + |
| 111 | + assert process_number.("42") == ___ |
| 112 | + assert process_number.("abc") == ___ |
| 113 | + end |
| 114 | + |
| 115 | + koan "Conditional pipes can use if/unless" do |
| 116 | + process_string = fn str, should_upcase -> |
| 117 | + str |
| 118 | + |> String.trim() |
| 119 | + |> then(&if should_upcase, do: String.upcase(&1), else: &1) |
| 120 | + |> String.split(" ") |
| 121 | + end |
| 122 | + |
| 123 | + assert process_string.(" hello world ", true) == ___ |
| 124 | + assert process_string.(" hello world ", false) == ___ |
| 125 | + end |
| 126 | + |
| 127 | + koan "Pipes work great with Enum functions for data processing" do |
| 128 | + sales_data = [ |
| 129 | + %{product: "Widget", amount: 100, month: "Jan"}, |
| 130 | + %{product: "Gadget", amount: 200, month: "Jan"}, |
| 131 | + %{product: "Widget", amount: 150, month: "Feb"}, |
| 132 | + %{product: "Gadget", amount: 180, month: "Feb"} |
| 133 | + ] |
| 134 | + |
| 135 | + widget_total = |
| 136 | + sales_data |
| 137 | + |> Enum.filter(&(&1.product == "Widget")) |
| 138 | + |> Enum.map(& &1.amount) |
| 139 | + |> Enum.sum() |
| 140 | + |
| 141 | + assert widget_total == ___ |
| 142 | + end |
| 143 | + |
| 144 | + koan "Tap lets you perform side effects without changing the pipeline" do |
| 145 | + result = |
| 146 | + [1, 2, 3] |
| 147 | + |> Enum.map(&(&1 * 2)) |
| 148 | + |> tap(&IO.inspect(&1, label: "After doubling")) |
| 149 | + |> Enum.sum() |
| 150 | + |
| 151 | + assert result == ___ |
| 152 | + end |
| 153 | + |
| 154 | + koan "Multiple transformations can be chained elegantly" do |
| 155 | + text = "The quick brown fox dumped over the lazy dog" |
| 156 | + |
| 157 | + word_stats = |
| 158 | + text |
| 159 | + |> String.downcase() |
| 160 | + |> String.split(" ") |
| 161 | + |> Enum.group_by(&String.first/1) |
| 162 | + |> Enum.map(fn {letter, words} -> {letter, length(words)} end) |
| 163 | + |> Enum.into(%{}) |
| 164 | + |
| 165 | + assert word_stats["d"] == ___ |
| 166 | + assert word_stats["t"] == ___ |
| 167 | + assert word_stats["q"] == ___ |
| 168 | + end |
| 169 | + |
| 170 | + koan "Pipes can be used in function definitions for clean APIs" do |
| 171 | + defmodule TextProcessor do |
| 172 | + def clean_and_count(text) do |
| 173 | + text |
| 174 | + |> String.trim() |
| 175 | + |> String.downcase() |
| 176 | + |> String.replace(~r/[^\w\s]/, "") |
| 177 | + |> String.split() |
| 178 | + |> length() |
| 179 | + end |
| 180 | + end |
| 181 | + |
| 182 | + assert TextProcessor.clean_and_count(" Hello, World! How are you? ") == ___ |
| 183 | + end |
| 184 | + |
| 185 | + koan "Error handling can be integrated into pipelines" do |
| 186 | + safe_divide = fn |
| 187 | + {x, 0} -> {:error, :division_by_zero} |
| 188 | + {x, y} -> {:ok, x / y} |
| 189 | + end |
| 190 | + |
| 191 | + pipeline = fn x, y -> |
| 192 | + {x, y} |
| 193 | + |> safe_divide.() |
| 194 | + |> case do |
| 195 | + {:ok, result} -> "Result: #{result}" |
| 196 | + {:error, reason} -> "Error: #{reason}" |
| 197 | + end |
| 198 | + end |
| 199 | + |
| 200 | + assert pipeline.(10, 2) == ___ |
| 201 | + assert pipeline.(10, 0) == ___ |
| 202 | + end |
| 203 | +end |
0 commit comments