Skip to content

Commit 5865e4f

Browse files
committed
Add koans for pipe operator
1 parent f231d86 commit 5865e4f

File tree

2 files changed

+230
-0
lines changed

2 files changed

+230
-0
lines changed

lib/koans/23_pipe_operator.ex

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
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
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
defmodule PipeOperatorTests do
2+
use ExUnit.Case
3+
import TestHarness
4+
5+
test "Pipe Operator" do
6+
answers = [
7+
"HELLO-WORLD",
8+
"hello_world",
9+
[6, 8, 10],
10+
"hello, world",
11+
20,
12+
"1-2-3",
13+
["Alice", "Charlie"],
14+
["QUICK", "BROWN", "JUMPS"],
15+
[a: 2, b: 4, c: 6],
16+
{:multiple, [{:ok, 84}, {:error, :invalid_number}]},
17+
{:multiple, [["HELLO", "WORLD"], ["hello", "world"]]},
18+
250,
19+
12,
20+
{:multiple, [2, 2, 1]},
21+
5,
22+
{:multiple, ["Result: 5.0", "Error: division_by_zero"]}
23+
]
24+
25+
test_all(PipeOperator, answers)
26+
end
27+
end

0 commit comments

Comments
 (0)