diff --git a/.gitignore b/.gitignore index 755b6055..da26a192 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /deps erl_crash.dump *.ez +.tool-versions diff --git a/CLAUDE_THOUGHTS_ON_KOANS.md b/CLAUDE_THOUGHTS_ON_KOANS.md new file mode 100644 index 00000000..04a2a530 --- /dev/null +++ b/CLAUDE_THOUGHTS_ON_KOANS.md @@ -0,0 +1,113 @@ +# Claude's Analysis of Elixir Koans + +## Overall Assessment + +The Elixir koans provide a solid foundation for learning Elixir's core concepts through hands-on practice. The progression from basic data types to advanced concurrency concepts follows a logical learning path that builds knowledge incrementally. + +## Strengths + +### 1. **Excellent Progression and Coverage** +- Well-structured from fundamentals (equalities, strings, numbers) to advanced topics (processes, GenServers, protocols) +- Covers all essential Elixir data types and concepts systematically +- Good balance between breadth and depth + +### 2. **Interactive Learning Approach** +- The fill-in-the-blank (`___`) format encourages active engagement +- Immediate feedback through test execution +- Zen-like koan naming creates an engaging learning atmosphere + +### 3. **Strong Foundation Building** +- **Basic Types**: Numbers, strings, atoms, booleans are well covered +- **Data Structures**: Comprehensive coverage of lists, tuples, maps, keyword lists, MapSets, and structs +- **Advanced Features**: Pattern matching, functions, enums, and comprehensions are thoughtfully presented + +### 4. **Concurrency Excellence** +- Outstanding coverage of Elixir's actor model with processes, Tasks, Agents, and GenServers +- Practical examples showing message passing, state management, and supervision +- Good introduction to OTP concepts + +## Areas for Improvement + +### 1. **Missing Fundamental Concepts** +- **Pipe Operator**: Only briefly mentioned in functions.ex:104-111, but deserves dedicated coverage as it's idiomatic Elixir +- **with Statement**: Missing entirely - important for error handling and nested operations +- **Case/Cond/If Statements**: Only case is briefly shown in pattern matching +- **Guard Clauses**: Mentioned in functions but could use more comprehensive coverage +- **Binary Pattern Matching**: Missing - important for working with binary data + +### 2. **Limited Error Handling** +- Only basic error tuple patterns (`{:ok, value}`, `{:error, reason}`) are shown +- Missing `try/catch/rescue/after` constructs +- No coverage of custom exception types +- Could benefit from more comprehensive error handling patterns + +### 3. **Module System Gaps** +- Basic module definition shown but missing: + - Module attributes beyond `@moduledoc` + - Import/alias/require directives + - Module compilation hooks + - Behaviors beyond GenServer + +### 4. **Syntax and Language Features** +- **Documentation**: No coverage of `@doc` or doctests +- **Typespecs**: Missing `@spec` and `@type` - important for larger codebases +- **Macros**: Not covered (though perhaps too advanced for koans) +- **Use/Import/Alias**: Mentioned but not explained + +### 5. **Practical Application** +- Most examples are abstract - could benefit from more real-world scenarios +- Missing file I/O operations +- No coverage of common patterns like supervision trees +- HTTP client/server basics could be valuable + +## Outdated or Problematic Areas + +### 1. **Syntax Updates Needed** +- All syntax appears current for modern Elixir (1.14+) +- No deprecated functions or patterns identified + +### 2. **Best Practices Alignment** +- Code follows current Elixir style guidelines +- Function definitions and module structures are idiomatic + +### 3. **Minor Issues** +- Line 113 in Numbers.ex uses pattern matching syntax that's slightly advanced for its position +- Some variable names could be more descriptive in complex examples + +## Recommended Additions + +### 1. **New Koans to Add** +``` +21_control_flow.ex # if/unless/cond/case comprehensive coverage +22_error_handling.ex # try/catch/rescue/after, error tuples +23_pipe_operator.ex # |>, then/2, comprehensive piping patterns +24_with_statement.ex # with clauses, error handling patterns +25_binary_matching.ex # <<>>, binary patterns, string manipulation +26_module_attributes.ex # @doc, @spec, @type, compile-time attributes +27_io_and_files.ex # File operations, IO operations +28_otp_behaviors.ex # Custom behaviors, supervision basics +``` + +### 2. **Enhanced Existing Koans** +- **Functions**: Add more pipe operator examples and capture syntax variations +- **Pattern Matching**: Include more binary pattern matching examples +- **GenServers**: Add supervision and error handling examples +- **Enums**: Include Stream module basics for lazy evaluation + +### 3. **Pedagogical Improvements** +- Add more real-world context to abstract examples +- Include common pitfalls and "gotcha" moments +- Add exercises that build on previous koans +- Include performance considerations where relevant + +## Conclusion + +The Elixir koans are well-crafted and provide excellent coverage of Elixir's core concepts. They successfully teach the fundamentals and introduce advanced topics in a logical progression. The main gaps are in practical error handling, advanced control flow, and some modern Elixir idioms. + +**Recommendation**: These koans do a good job introducing Elixir basics. The most impactful improvements would be: +1. Adding dedicated coverage for the pipe operator and `with` statement +2. Expanding error handling beyond basic tuple patterns +3. Including more real-world, practical examples +4. Adding binary pattern matching for string/data processing + +The current koans provide a solid foundation, but learners would benefit from supplementary material covering the missing concepts before moving to production Elixir development. \ No newline at end of file diff --git a/lib/koans/12_pattern_matching.ex b/lib/koans/12_pattern_matching.ex index e43c92b7..76dda2d5 100644 --- a/lib/koans/12_pattern_matching.ex +++ b/lib/koans/12_pattern_matching.ex @@ -83,15 +83,15 @@ defmodule PatternMatching do end koan "Errors are shaped differently than successful results" do - dog = %{type: "dog"} + dog = %{type: "barking"} - result = + type = case Map.fetch(dog, :type) do {:ok, value} -> value :error -> "not present" end - assert result == ___ + assert type == ___ end defmodule Animal do @@ -166,4 +166,42 @@ defmodule PatternMatching do ^a = ___ end end + + koan "Pattern matching works with nested data structures" do + user = %{ + profile: %{ + personal: %{name: "Alice", age: 30}, + settings: %{theme: "dark", notifications: true} + } + } + + %{profile: %{personal: %{age: age}, settings: %{theme: theme}}} = user + assert age == ___ + assert theme == ___ + end + + koan "Lists can be pattern matched with head and tail" do + numbers = [1, 2, 3, 4, 5] + + [first, second | rest] = numbers + assert first == ___ + assert second == ___ + assert rest == ___ + + [head | _tail] = numbers + assert head == ___ + end + + koan "Pattern matching can extract values from function return tuples" do + divide = fn + _, 0 -> {:error, :division_by_zero} + x, y -> {:ok, x / y} + end + + {:ok, result} = divide.(10, 2) + assert result == ___ + + {:error, reason} = divide.(10, 0) + assert reason == ___ + end end diff --git a/lib/koans/13_functions.ex b/lib/koans/13_functions.ex index a9c63500..00258dff 100644 --- a/lib/koans/13_functions.ex +++ b/lib/koans/13_functions.ex @@ -110,6 +110,28 @@ defmodule Functions do assert result == ___ end + koan "Pipes make data transformation pipelines readable" do + numbers = [1, 2, 3, 4, 5] + + result = + numbers + |> Enum.filter(&(&1 > 2)) + |> Enum.map(&(&1 * 2)) + |> Enum.sum() + + assert result == ___ + + user_input = " Hello World " + + cleaned = + user_input + |> String.trim() + |> String.downcase() + |> String.replace(" ", "_") + + assert cleaned == ___ + end + koan "Conveniently keyword lists can be used for function options" do transform = fn str, opts -> if opts[:upcase] do @@ -122,4 +144,16 @@ defmodule Functions do assert transform.("good", upcase: true) == ___ assert transform.("good", upcase: false) == ___ end + + koan "Anonymous functions can use the & capture syntax for very concise definitions" do + add_one = &(&1 + 1) + multiply_by_two = &(&1 * 2) + + result = 5 |> add_one.() |> multiply_by_two.() + assert result == ___ + + # You can also capture existing functions + string_length = &String.length/1 + assert string_length.("hello") == ___ + end end diff --git a/lib/koans/14_enums.ex b/lib/koans/14_enums.ex index e158aa7c..23c4e9ca 100644 --- a/lib/koans/14_enums.ex +++ b/lib/koans/14_enums.ex @@ -8,8 +8,18 @@ defmodule Enums do assert Enum.count([1, 2, 3]) == ___ end - koan "Depending on the type, it counts pairs" do - assert Enum.count(%{a: :foo, b: :bar}) == ___ + koan "Counting is similar to length" do + assert length([1, 2, 3]) == ___ + end + + koan "But it allows you to count certain elements" do + assert Enum.count([1, 2, 3], &(&1 == 2)) == ___ + end + + koan "Depending on the type, it counts pairs while length does not" do + map = %{a: :foo, b: :bar} + assert Enum.count(map) == ___ + assert_raise ___, fn -> length(map) end end def less_than_five?(n), do: n < 5 @@ -34,7 +44,7 @@ defmodule Enums do def multiply_by_ten(n), do: 10 * n - koan "Map converts each element of a list by running some function with it" do + koan "Mapping converts each element of a list by running some function with it" do assert Enum.map([1, 2, 3], &multiply_by_ten/1) == ___ end @@ -66,7 +76,7 @@ defmodule Enums do assert Enum.zip(letters, numbers) == ___ end - koan "When you want to find that one pesky element" do + koan "When you want to find that one pesky element, it returns the first" do assert Enum.find([1, 2, 3, 4], &even?/1) == ___ end @@ -83,4 +93,38 @@ defmodule Enums do koan "Collapse an entire list of elements down to a single one by repeating a function." do assert Enum.reduce([1, 2, 3], 0, fn element, accumulator -> element + accumulator end) == ___ end + + koan "Enum.chunk_every splits lists into smaller lists of fixed size" do + assert Enum.chunk_every([1, 2, 3, 4, 5, 6], 2) == ___ + assert Enum.chunk_every([1, 2, 3, 4, 5], 3) == ___ + end + + koan "Enum.flat_map transforms and flattens in one step" do + result = + [1, 2, 3] + |> Enum.flat_map(&[&1, &1 * 10]) + + assert result == ___ + end + + koan "Enum.group_by organizes elements by a grouping function" do + words = ["apple", "banana", "cherry", "apricot", "blueberry"] + grouped = Enum.group_by(words, &String.first/1) + + assert grouped["a"] == ___ + assert grouped["b"] == ___ + end + + koan "Stream provides lazy enumeration for large datasets" do + # Streams are lazy - they don't execute until you call Enum on them + stream = + 1..1_000_000 + |> Stream.filter(&even?/1) + |> Stream.map(&(&1 * 2)) + |> Stream.take(3) + + # Nothing has been computed yet! + result = Enum.to_list(stream) + assert result == ___ + end end diff --git a/lib/koans/18_genservers.ex b/lib/koans/18_genservers.ex index 752a4f4f..a05ceab2 100644 --- a/lib/koans/18_genservers.ex +++ b/lib/koans/18_genservers.ex @@ -160,4 +160,86 @@ defmodule GenServers do :ok = Laptop.stop() end + + defmodule TimeoutServer do + use GenServer + + def start_link(timeout) do + GenServer.start_link(__MODULE__, timeout, name: __MODULE__) + end + + def init(timeout) do + {:ok, %{count: 0}, timeout} + end + + def get_count do + GenServer.call(__MODULE__, :get_count) + end + + def handle_call(:get_count, _from, state) do + {:reply, state.count, state} + end + + def handle_info(:timeout, state) do + new_state = %{state | count: state.count + 1} + {:noreply, new_state} + end + end + + koan "GenServers can handle info messages and timeouts" do + {:ok, _pid} = TimeoutServer.start_link(100) + # Wait for timeout to occur + :timer.sleep(101) + count = TimeoutServer.get_count() + assert count == ___ + + GenServer.stop(TimeoutServer) + end + + defmodule CrashableServer do + use GenServer + + def start_link(initial) do + GenServer.start_link(__MODULE__, initial, name: __MODULE__) + end + + def init(initial) do + {:ok, initial} + end + + def crash do + GenServer.cast(__MODULE__, :crash) + end + + def get_state do + GenServer.call(__MODULE__, :get_state) + end + + def handle_call(:get_state, _from, state) do + {:reply, state, state} + end + + def handle_cast(:crash, _state) do + raise "Intentional crash for testing" + end + end + + koan "GenServers can be supervised and restarted" do + # Start under a supervisor + children = [{CrashableServer, "the state"}] + {:ok, supervisor} = Supervisor.start_link(children, strategy: :one_for_one) + + # Server should be running + initial_state = CrashableServer.get_state() + assert initial_state == ___ + + :ok = CrashableServer.crash() + # Wait for recovery + :timer.sleep(100) + + state_after_crash_recovery = CrashableServer.get_state() + assert state_after_crash_recovery == ___ + + Supervisor.stop(supervisor) + end end diff --git a/lib/koans/21_control_flow.ex b/lib/koans/21_control_flow.ex new file mode 100644 index 00000000..166e3bfe --- /dev/null +++ b/lib/koans/21_control_flow.ex @@ -0,0 +1,156 @@ +defmodule ControlFlow do + @moduledoc false + use Koans + + @intro "Control Flow - Making decisions and choosing paths" + + koan "If statements evaluate conditions" do + result = if true, do: "yes", else: "no" + assert result == ___ + end + + koan "If can be written in block form" do + result = + if 1 + 1 == 2 do + "math works" + else + "math is broken" + end + + assert result == ___ + end + + koan "Unless is the opposite of if" do + result = unless false, do: "will execute", else: "will not execute" + assert result == ___ + end + + koan "Nil and false are falsy, everything else is truthy" do + assert if(nil, do: "truthy", else: "falsy") == ___ + assert if(false, do: "truthy", else: "falsy") == ___ + assert if(0, do: "truthy", else: "falsy") == ___ + assert if("", do: "truthy", else: "falsy") == ___ + assert if([], do: "truthy", else: "falsy") == ___ + end + + koan "Case matches against patterns" do + result = + case {1, 2, 3} do + {4, 5, 6} -> "no match" + {1, x, 3} -> "matched with x = #{x}" + end + + assert result == ___ + end + + koan "Case can have multiple clauses with different patterns" do + check_number = fn x -> + case x do + 0 -> "zero" + n when n > 0 -> "positive" + n when n < 0 -> "negative" + end + end + + assert check_number.(5) == ___ + assert check_number.(0) == ___ + assert check_number.(-3) == ___ + end + + koan "Case clauses are tried in order until one matches" do + check_list = fn list -> + case list do + [] -> "empty" + [_] -> "one element" + [_, _] -> "two elements" + _ -> "many elements" + end + end + + assert check_list.([]) == ___ + assert check_list.([:a]) == ___ + assert check_list.([:a, :b]) == ___ + assert check_list.([:a, :b, :c, :d]) == ___ + end + + koan "Cond evaluates conditions until one is truthy" do + temperature = 25 + + weather = + cond do + temperature < 0 -> "freezing" + temperature < 10 -> "cold" + temperature < 25 -> "cool" + temperature < 30 -> "warm" + true -> "hot" + end + + assert weather == ___ + end + + koan "Cond requires at least one clause to be true" do + safe_divide = fn x, y -> + cond do + y == 0 -> {:error, "division by zero"} + true -> {:ok, x / y} + end + end + + assert safe_divide.(10, 2) == ___ + assert safe_divide.(10, 0) == ___ + end + + koan "Case can destructure complex patterns" do + parse_response = fn response -> + case response do + {:ok, %{status: 200, body: body}} -> "Success: #{body}" + {:ok, %{status: status}} when status >= 400 -> "Client error: #{status}" + {:ok, %{status: status}} when status >= 500 -> "Server error: #{status}" + {:error, reason} -> "Request failed: #{reason}" + end + end + + assert parse_response.({:ok, %{status: 200, body: "Hello"}}) == ___ + assert parse_response.({:ok, %{status: 404}}) == ___ + assert parse_response.({:error, :timeout}) == ___ + end + + koan "Guards in case can use complex expressions" do + categorize = fn number -> + case number do + n when is_integer(n) and n > 0 and rem(n, 2) == 0 -> "positive even integer" + n when is_integer(n) and n > 0 and rem(n, 2) == 1 -> "positive odd integer" + n when is_integer(n) and n < 0 -> "negative integer" + n when is_float(n) -> "float" + _ -> "other" + end + end + + assert categorize.(4) == ___ + assert categorize.(3) == ___ + assert categorize.(-5) == ___ + assert categorize.(3.14) == ___ + assert categorize.("hello") == ___ + end + + koan "Multiple conditions can be checked in sequence" do + process_user = fn user -> + if user.active do + if user.verified do + if user.premium do + "premium verified active user" + else + "verified active user" + end + else + "unverified active user" + end + else + "inactive user" + end + end + + user = %{active: true, verified: true, premium: false} + assert process_user.(user) == ___ + end +end diff --git a/lib/koans/22_error_handling.ex b/lib/koans/22_error_handling.ex new file mode 100644 index 00000000..a26cd02e --- /dev/null +++ b/lib/koans/22_error_handling.ex @@ -0,0 +1,201 @@ +defmodule ErrorHandling do + @moduledoc false + use Koans + + @intro "Error Handling - Dealing gracefully with things that go wrong" + + koan "Result tuples are a common pattern for success and failure" do + parse_number = fn string -> + case Integer.parse(string) do + {number, ""} -> {:ok, number} + _ -> {:error, :invalid_format} + end + end + + assert parse_number.("123") == ___ + assert parse_number.("abc") == ___ + end + + koan "Pattern matching makes error handling elegant" do + divide = fn x, y -> + case y do + 0 -> {:error, :division_by_zero} + _ -> {:ok, x / y} + end + end + + result = + case divide.(10, 2) do + {:ok, value} -> "Result: #{value}" + {:error, reason} -> "Error: #{reason}" + end + + assert result == ___ + end + + koan "Try-rescue catches runtime exceptions" do + result = + try do + 10 / 0 + rescue + ArithmeticError -> "Cannot divide by zero!" + end + + assert result == ___ + end + + koan "Try-rescue can catch specific exception types" do + safe_list_access = fn list, index -> + try do + {:ok, Enum.at(list, index)} + rescue + FunctionClauseError -> {:error, :invalid_argument} + e in Protocol.UndefinedError -> {:error, "#{e.value} is not a list"} + end + end + + assert safe_list_access.([1, 2, 3], 1) == ___ + assert safe_list_access.([1, 2, 3], "a") == ___ + assert safe_list_access.("abc", 0) == ___ + end + + koan "Multiple rescue clauses handle different exceptions" do + risky_operation = fn input -> + try do + case input do + "divide" -> 10 / 0 + "access" -> Map.fetch!(%{}, :missing_key) + "convert" -> String.to_integer("not_a_number") + _ -> {:ok, "success"} + end + rescue + ArithmeticError -> {:error, :arithmetic} + KeyError -> {:error, :missing_key} + ArgumentError -> {:error, :invalid_argument} + end + end + + assert risky_operation.("divide") == ___ + assert risky_operation.("access") == ___ + assert risky_operation.("convert") == ___ + assert risky_operation.("safe") == ___ + end + + koan "Try-catch handles thrown values" do + result = + try do + throw(:early_return) + "this won't be reached" + catch + :early_return -> "caught thrown value" + end + + assert result == ___ + end + + koan "After clause always executes for cleanup" do + cleanup_called = + try do + raise "something went wrong" + rescue + RuntimeError -> :returned_value + after + IO.puts("Executed but not returned") + end + + assert cleanup_called == ___ + end + + koan "After executes even when there's no error" do + {result, value} = + try do + {:success, "it worked"} + after + IO.puts("Executed but not returned") + end + + assert result == ___ + assert value == ___ + end + + defmodule CustomError do + defexception message: "something custom went wrong" + end + + koan "Custom exceptions can be defined and raised" do + result = + try do + raise CustomError, message: "custom failure" + rescue + e in CustomError -> "caught custom error: #{e.message}" + end + + assert result == ___ + end + + koan "Bang functions raise exceptions on failure" do + result = + try do + Map.fetch!(%{a: 1}, :b) + rescue + KeyError -> "key not found" + end + + assert result == ___ + end + + koan "Exit signals can be caught and handled" do + result = + try do + exit(:normal) + catch + :exit, :normal -> "caught normal exit" + end + + assert result == ___ + end + + koan "Multiple clauses can handle different error patterns" do + handle_database_operation = fn operation -> + try do + case operation do + :connection_error -> raise "connection failed" + :timeout -> exit(:timeout) + :invalid_query -> throw(:bad_query) + :success -> {:ok, "data retrieved"} + end + rescue + e in RuntimeError -> {:error, {:exception, e.message}} + catch + :exit, :timeout -> {:error, :timeout} + :bad_query -> {:error, :invalid_query} + end + end + + assert handle_database_operation.(:connection_error) == ___ + assert handle_database_operation.(:timeout) == ___ + assert handle_database_operation.(:invalid_query) == ___ + assert handle_database_operation.(:success) == ___ + end + + koan "Error information can be preserved and enriched" do + enriched_error = fn -> + try do + String.to_integer("not a number") + rescue + e in ArgumentError -> + {:error, + %{ + type: :conversion_error, + original: e, + context: "user input processing", + message: "Failed to convert string to integer" + }} + end + end + + {:error, error_info} = enriched_error.() + assert error_info.type == ___ + assert error_info.context == ___ + end +end diff --git a/lib/koans/23_pipe_operator.ex b/lib/koans/23_pipe_operator.ex new file mode 100644 index 00000000..f03e0ff2 --- /dev/null +++ b/lib/koans/23_pipe_operator.ex @@ -0,0 +1,203 @@ +defmodule PipeOperator do + @moduledoc false + use Koans + + @intro "The Pipe Operator - Making data transformation elegant and readable" + + koan "The pipe operator passes the result of one function to the next" do + result = + "hello world" + |> String.upcase() + |> String.split(" ") + |> Enum.join("-") + + assert result == ___ + end + + koan "Without pipes, nested function calls can be hard to read" do + nested_result = Enum.join(String.split(String.downcase("Hello World"), " "), "_") + piped_result = "Hello World" |> String.downcase() |> String.split(" ") |> Enum.join("_") + + assert nested_result == piped_result + assert piped_result == ___ + end + + koan "Pipes pass the result as the first argument to the next function" do + result = + [1, 2, 3, 4, 5] + |> Enum.filter(&(&1 > 2)) + |> Enum.map(&(&1 * 2)) + + assert result == ___ + end + + koan "Additional arguments can be passed to piped functions" do + result = + "hello world" + |> String.split(" ") + |> Enum.join(", ") + + assert result == ___ + end + + koan "Pipes work with anonymous functions too" do + double = fn x -> x * 2 end + add_ten = fn x -> x + 10 end + + result = + 5 + |> double.() + |> add_ten.() + + assert result == ___ + end + + koan "You can pipe into function captures" do + result = + [1, 2, 3] + |> Enum.map(&Integer.to_string/1) + |> Enum.join("-") + + assert result == ___ + end + + koan "Complex data transformations become readable with pipes" do + users = [ + %{name: "Bob", age: 25, active: false}, + %{name: "Charlie", age: 35, active: true}, + %{name: "Alice", age: 30, active: true} + ] + + active_names = + users + |> Enum.filter(& &1.active) + |> Enum.map(& &1.name) + |> Enum.sort() + + assert active_names == ___ + end + + koan "Pipes can be split across multiple lines for readability" do + result = + "the quick brown fox jumps over the lazy dog" + |> String.split(" ") + |> Enum.filter(&(String.length(&1) > 3)) + |> Enum.map(&String.upcase/1) + |> Enum.take(3) + + assert result == ___ + end + + # TODO: Fix this example. It doesn't illustrate the point well. + 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 + result = + [1, 2, 3] + |> Enum.map(&(&1 * 2)) + |> then(&Enum.zip([:a, :b, :c], &1)) + + assert result == ___ + end + + koan "Pipes can be used with case statements" do + process_number = fn x -> + x + |> Integer.parse() + |> case do + {num, ""} -> {:ok, num * 2} + _ -> {:error, :invalid_number} + end + end + + assert process_number.("42") == ___ + assert process_number.("abc") == ___ + end + + koan "Conditional pipes can use if/unless" do + process_string = fn str, should_upcase -> + str + |> String.trim() + |> then(&if should_upcase, do: String.upcase(&1), else: &1) + |> String.split(" ") + end + + assert process_string.(" hello world ", true) == ___ + assert process_string.(" hello world ", false) == ___ + end + + koan "Pipes work great with Enum functions for data processing" do + sales_data = [ + %{product: "Widget", amount: 100, month: "Jan"}, + %{product: "Gadget", amount: 200, month: "Jan"}, + %{product: "Widget", amount: 150, month: "Feb"}, + %{product: "Gadget", amount: 180, month: "Feb"} + ] + + widget_total = + sales_data + |> Enum.filter(&(&1.product == "Widget")) + |> Enum.map(& &1.amount) + |> Enum.sum() + + assert widget_total == ___ + end + + koan "Tap lets you perform side effects without changing the pipeline" do + result = + [1, 2, 3] + |> Enum.map(&(&1 * 2)) + |> tap(&IO.inspect(&1, label: "After doubling")) + |> Enum.sum() + + assert result == ___ + end + + koan "Multiple transformations can be chained elegantly" do + text = "The quick brown fox dumped over the lazy dog" + + word_stats = + text + |> String.downcase() + |> String.split(" ") + |> Enum.group_by(&String.first/1) + |> Enum.map(fn {letter, words} -> {letter, length(words)} end) + |> Enum.into(%{}) + + assert word_stats["d"] == ___ + assert word_stats["t"] == ___ + assert word_stats["q"] == ___ + end + + koan "Pipes can be used in function definitions for clean APIs" do + defmodule TextProcessor do + def clean_and_count(text) do + text + |> String.trim() + |> String.downcase() + |> String.replace(~r/[^\w\s]/, "") + |> String.split() + |> length() + end + end + + assert TextProcessor.clean_and_count(" Hello, World! How are you? ") == ___ + end + + koan "Error handling can be integrated into pipelines" do + safe_divide = fn + {x, 0} -> {:error, :division_by_zero} + {x, y} -> {:ok, x / y} + end + + pipeline = fn x, y -> + {x, y} + |> safe_divide.() + |> case do + {:ok, result} -> "Result: #{result}" + {:error, reason} -> "Error: #{reason}" + end + end + + assert pipeline.(10, 2) == ___ + assert pipeline.(10, 0) == ___ + end +end diff --git a/lib/koans/24_with_statement.ex b/lib/koans/24_with_statement.ex new file mode 100644 index 00000000..251376e4 --- /dev/null +++ b/lib/koans/24_with_statement.ex @@ -0,0 +1,224 @@ +defmodule WithStatement do + @moduledoc false + use Koans + + @intro "The With Statement - Elegant error handling and happy path programming" + + koan "With lets you chain operations that might fail" do + parse_and_add = fn str1, str2 -> + with {:ok, a} <- Integer.parse(str1), + {:ok, b} <- Integer.parse(str2) do + {:ok, a + b} + else + :error -> {:error, :invalid_number} + end + end + + assert parse_and_add.("5", "4") == ___ + assert parse_and_add.("abc", "1") == ___ + end + + koan "With short-circuits on the first non-matching pattern" do + process_user = fn user_data -> + with {:ok, name} <- Map.fetch(user_data, :name), + {:ok, age} <- Map.fetch(user_data, :age), + true <- age >= 18 do + {:ok, "Adult user: #{name}"} + else + :error -> {:error, :missing_data} + false -> {:error, :underage} + end + end + + assert process_user.(%{name: "Alice", age: 25}) == ___ + assert process_user.(%{name: "Bob", age: 16}) == ___ + assert process_user.(%{age: 25}) == ___ + end + + defp safe_divide(_, 0), do: {:error, :division_by_zero} + defp safe_divide(x, y), do: {:ok, x / y} + + defp safe_sqrt(x) when x < 0, do: {:error, :negative_sqrt} + defp safe_sqrt(x), do: {:ok, :math.sqrt(x)} + + koan "With can handle multiple different error patterns" do + divide_and_sqrt = fn x, y -> + with {:ok, division} <- safe_divide(x, y), + {:ok, sqrt} <- safe_sqrt(division) do + {:ok, sqrt} + else + {:error, :division_by_zero} -> {:error, "Cannot divide by zero"} + {:error, :negative_sqrt} -> {:error, "Cannot take square root of negative number"} + end + end + + assert divide_and_sqrt.(16, 4) == ___ + assert divide_and_sqrt.(10, 0) == ___ + assert divide_and_sqrt.(-16, 4) == ___ + end + + koan "With works great for nested data extraction" do + get_user_email = fn data -> + with {:ok, user} <- Map.fetch(data, :user), + {:ok, profile} <- Map.fetch(user, :profile), + {:ok, email} <- Map.fetch(profile, :email), + true <- String.contains?(email, "@") do + {:ok, email} + else + :error -> {:error, :missing_data} + false -> {:error, :invalid_email} + end + end + + valid_data = %{ + user: %{ + profile: %{ + email: "user@example.com" + } + } + } + + invalid_email_data = %{ + user: %{ + profile: %{ + email: "notanemail" + } + } + } + + assert get_user_email.(valid_data) == ___ + assert get_user_email.(invalid_email_data) == ___ + assert get_user_email.(%{}) == ___ + end + + koan "With can combine pattern matching with guards" do + process_number = fn input -> + with {:ok, num} <- Integer.parse(input), + true <- num > 0, + result when result < 1000 <- num * 10 do + {:ok, result} + else + :error -> {:error, :not_a_number} + false -> {:error, :not_positive} + result when result >= 1000 -> {:error, :result_too_large} + end + end + + assert process_number.("5") == ___ + assert process_number.("-5") == ___ + assert process_number.("150") == ___ + assert process_number.("abc") == ___ + end + + koan "With clauses can have side effects and assignments" do + register_user = fn user_data -> + with {:ok, email} <- validate_email(user_data[:email]), + {:ok, password} <- validate_password(user_data[:password]), + hashed_password = hash_password(password), + {:ok, user} <- save_user(email, hashed_password) do + {:ok, user} + else + {:error, reason} -> {:error, reason} + end + end + + user_data = %{email: "test@example.com", password: "secure123"} + assert register_user.(user_data) == ___ + end + + defp validate_email(email) when is_binary(email) and byte_size(email) > 0 do + if String.contains?(email, "@"), do: {:ok, email}, else: {:error, :invalid_email} + end + + defp validate_email(_), do: {:error, :invalid_email} + + defp validate_password(password) when is_binary(password) and byte_size(password) >= 6 do + {:ok, password} + end + + defp validate_password(_), do: {:error, :weak_password} + + defp hash_password(password), do: "hashed_" <> password + + defp save_user(email, hashed_password) do + {:ok, %{id: 1, email: email, password: hashed_password}} + end + + koan "With can be used without an else clause for simpler cases" do + simple_calculation = fn x, y -> + with num1 when is_number(num1) <- x, + num2 when is_number(num2) <- y do + num1 + num2 + end + end + + assert simple_calculation.(5, 3) == ___ + # When pattern doesn't match and no else, returns the non-matching value + assert simple_calculation.("5", 3) == ___ + end + + # TODO: Is there any reason the mix the case and with here? Can't they be + # combined? I'm not sure what the point is. + koan "With integrates beautifully with pipe operators" do + process_order = fn order_data -> + order_data + |> validate_order() + |> case do + {:ok, order} -> + with {:ok, payment} <- process_payment(order), + {:ok, shipment} <- create_shipment(order, payment) do + {:ok, %{order: order, payment: payment, shipment: shipment}} + end + + error -> + error + end + end + + valid_order = %{item: "book", price: 20, customer: "alice"} + assert process_order.(valid_order) == ___ + end + + defp validate_order(%{item: item, price: price, customer: customer}) + when is_binary(item) and is_number(price) and price > 0 and is_binary(customer) do + {:ok, %{item: item, price: price, customer: customer, id: 123}} + end + + defp validate_order(_), do: {:error, :invalid_order} + + defp process_payment(%{price: price}) when price > 0 do + {:ok, %{amount: price, status: "paid", id: 456}} + end + + defp create_shipment(%{customer: customer}, %{status: "paid"}) do + {:ok, %{customer: customer, status: "shipped", tracking: "ABC123"}} + end + + koan "With can handle complex nested error scenarios" do + complex_workflow = fn data -> + with {:ok, step1} <- step_one(data), + {:ok, step2} <- step_two(step1), + {:ok, step3} <- step_three(step2) do + {:ok, step3} + else + {:error, :step1_failed} -> {:error, "Failed at step 1: invalid input"} + {:error, :step2_failed} -> {:error, "Failed at step 2: processing error"} + {:error, :step3_failed} -> {:error, "Failed at step 3: final validation error"} + other -> {:error, "Unexpected error: #{inspect(other)}"} + end + end + + assert complex_workflow.("valid") == ___ + assert complex_workflow.("step1_fail") == ___ + assert complex_workflow.("step2_fail") == ___ + end + + defp step_one("step1_fail"), do: {:error, :step1_failed} + defp step_one(data), do: {:ok, "step1_" <> data} + + defp step_two("step1_step2_fail"), do: {:error, :step2_failed} + defp step_two(data), do: {:ok, "step2_" <> data} + + defp step_three("step2_step1_step3_fail"), do: {:error, :step3_failed} + defp step_three(data), do: {:ok, "step3_" <> data} +end diff --git a/lib/koans/25_binary_matching.ex b/lib/koans/25_binary_matching.ex new file mode 100644 index 00000000..d2ae54ee --- /dev/null +++ b/lib/koans/25_binary_matching.ex @@ -0,0 +1,232 @@ +defmodule BinaryMatching do + @moduledoc false + use Koans + + @intro "Binary Pattern Matching - Working with raw bytes and binary data" + + koan "Binaries are sequences of bytes" do + binary = <<1, 2, 3>> + assert byte_size(binary) == ___ + end + + koan "Strings are UTF-8 encoded binaries" do + string = "hello" + assert is_binary(string) == ___ + assert byte_size(string) == ___ + end + + koan "You can pattern match on binary prefixes" do + <<"Hello", rest::binary>> = "Hello, World!" + assert rest == ___ + end + + koan "Binary pattern matching can extract specific bytes" do + <> = <<65, 66, 67, 68>> + assert first == ___ + assert second == ___ + assert rest == ___ + end + + koan "String pattern matching works with binary syntax" do + <<"HTTP/", version::binary-size(3), " ", status::binary-size(3), " ", message::binary>> = + "HTTP/1.1 200 OK" + + assert version == ___ + assert status == ___ + assert message == ___ + end + + koan "You can match on specific bit patterns" do + <> = <<200>> + assert flag == ___ + assert counter == ___ + end + + koan "Endianness can be specified for multi-byte integers" do + <> = <<1, 2>> + assert number == ___ + + <> = <<1, 2>> + assert number == ___ + end + + koan "You can construct binaries with specific values" do + binary = <<255, 0, 128>> + <> = binary + assert high == ___ + assert low == ___ + assert middle == ___ + end + + koan "Float values can be packed into binaries" do + <> = <<66, 246, 0, 0>> + assert Float.round(value, 1) == ___ + end + + # I think this is trying to cover https://hexdocs.pm/elixir/main/comprehensions.html#bitstring-generators + # but the syntax is apparently wrong... + # TODO: investigate + # koan "Binary comprehensions can create patterns" do + # result = for <>, byte > 2>>, do: byte * 2 + # assert result == ___ + # end + + # TODO: investigate syntax here. It's erroring currently + # koan "You can parse CSV-like data with binary matching" do + # parse_csv_line = fn line -> + # String.split(String.trim(line), ",") + # |> Enum.map(&String.trim/1) + # end + + # # But with binary matching for more control: + # parse_field = fn + # <<"\"", field::binary-size(n), "\"", _::binary>> when byte_size(field) > 0 -> field + # <> -> String.trim(field) + # end + + # result = parse_csv_line.("Alice, 30, Engineer") + # assert result == ___ + # end + + koan "IP address parsing with binary patterns" do + parse_ipv4 = fn ip_string -> + case String.split(ip_string, ".") do + [a, b, c, d] -> + <> + + _ -> + :error + end + end + + <> = parse_ipv4.("192.168.1.1") + assert a == ___ + assert b == ___ + assert c == ___ + assert d == ___ + end + + koan "Binary matching can validate data formats" do + is_png? = fn + <<137, 80, 78, 71, 13, 10, 26, 10, _::binary>> -> true + _ -> false + end + + png_header = <<137, 80, 78, 71, 13, 10, 26, 10, "fake data">> + jpeg_header = <<255, 216, 255, "fake data">> + + assert is_png?.(png_header) == ___ + assert is_png?.(jpeg_header) == ___ + end + + koan "You can extract length-prefixed strings" do + parse_length_string = fn + <> -> + {string, rest} + + _ -> + :error + end + + data = <<5, "Hello", "World">> + {extracted, remaining} = parse_length_string.(data) + assert extracted == ___ + assert remaining == ___ + end + + koan "Binary matching works with hexadecimal literals" do + <> = <<0xFF, 0x80, 0x00>> + assert red == ___ + assert green == ___ + assert blue == ___ + end + + koan "You can match variable-length binary data" do + extract_until_delimiter = fn binary, delimiter -> + case :binary.split(binary, delimiter) do + [a, b] -> {a, b} + [_] -> {binary, ""} + end + end + + {a, b} = extract_until_delimiter.("name=John&age=30", "&") + assert a == ___ + assert b == ___ + end + + koan "Binary matching can parse simple protocols" do + parse_message = fn + <<1, length::16, payload::binary-size(length)>> -> + {:text, payload} + + <<2, length::16, payload::binary-size(length)>> -> + {:binary, payload} + + <<3>> -> + :ping + + _ -> + :unknown + end + + text_msg = <<1, 0, 5, "Hello">> + ping_msg = <<3>> + + assert parse_message.(text_msg) == ___ + assert parse_message.(ping_msg) == ___ + end + + koan "String interpolation creates binaries" do + name = "Alice" + age = 30 + message = "Hello #{name}, you are #{age} years old" + + <<"Hello ", rest::binary>> = message + assert rest == ___ + end + + koan "Binary pattern matching can validate checksums" do + validate_checksum = fn <> -> + calculated = + data + |> :binary.bin_to_list() + |> Enum.sum() + |> rem(256) + + calculated == checksum + end + + # Data: [1,2,3,4], sum = 10, checksum = 10 + valid_packet = <<1, 2, 3, 4, 10>> + invalid_packet = <<1, 2, 3, 4, 20>> + + assert validate_checksum.(valid_packet) == ___ + assert validate_checksum.(invalid_packet) == ___ + end + + koan "You can work with null-terminated strings" do + parse_c_string = fn binary -> + case :binary.split(binary, <<0>>) do + [string, _rest] -> string + [string] -> string + end + end + + c_string = <<"Hello World", 0, "ignored">> + result = parse_c_string.(c_string) + assert result == ___ + end + + koan "Binary construction and pattern matching are symmetric" do + # Construction + packet = <<42::16, "Hello", 0>> + + # Deconstruction + <> = packet + + assert id == ___ + assert message == ___ + assert terminator == ___ + end +end diff --git a/lib/koans/26_module_attributes.ex b/lib/koans/26_module_attributes.ex new file mode 100644 index 00000000..76e8834b --- /dev/null +++ b/lib/koans/26_module_attributes.ex @@ -0,0 +1,217 @@ +defmodule ModuleAttributes do + @moduledoc """ + This module demonstrates various types of module attributes in Elixir. + Module attributes provide metadata and compile-time configuration. + """ + use Koans + + @intro "Module Attributes - Metadata, documentation, and compile-time values" + + # Compile-time constant + @default_timeout 5000 + + # Documentation attributes + @doc "A simple function that returns a greeting" + @spec greet(String.t()) :: String.t() + def greet(name) do + "Hello, #{name}!" + end + + koan "Module attributes can store compile-time constants" do + assert @default_timeout == ___ + end + + # Type specifications + @type user :: %{name: String.t(), age: integer()} + @type result :: {:ok, any()} | {:error, String.t()} + + @doc "Creates a new user with validation" + @spec create_user(String.t(), integer()) :: result() + def create_user(name, age) when is_binary(name) and is_integer(age) and age >= 0 do + {:ok, %{name: name, age: age}} + end + + def create_user(_, _), do: {:error, "Invalid user data"} + + koan "Module attributes can define custom types" do + user = %{name: "Alice", age: 30} + assert user.name == ___ + assert user.age == ___ + end + + # Accumulating attributes + @tag :important + @tag :deprecated + @tag :experimental + + koan "Some attributes accumulate values when defined multiple times" do + tags = Module.get_attribute(__MODULE__, :tag) + assert :important in tags == ___ + assert :experimental in tags == ___ + assert length(tags) == ___ + end + + # Dynamic attribute calculation + @compile_time :os.timestamp() + + koan "Attributes are evaluated at compile time" do + # This will be whatever timestamp was captured when the module compiled + assert is_tuple(@compile_time) == ___ + end + + # Attribute with default value pattern + @config Application.compile_env(:my_app, :config, %{timeout: 1000}) + + koan "Attributes can have default values from application config" do + assert @config.timeout == ___ + end + + # Using attributes in function heads + @max_retries 3 + + @doc "Retries an operation up to the configured maximum" + @spec retry_operation(function(), non_neg_integer()) :: any() + def retry_operation(operation, attempts \\ 0) + def retry_operation(operation, @max_retries), do: {:error, :max_retries_reached} + + def retry_operation(operation, attempts) do + case operation.() do + {:ok, result} -> {:ok, result} + {:error, _} -> retry_operation(operation, attempts + 1) + end + end + + koan "Attributes can be used in pattern matching in function definitions" do + failing_op = fn -> {:error, :simulated_failure} end + result = retry_operation(failing_op) + assert result == ___ + end + + # Custom attribute with register + Module.register_attribute(__MODULE__, :custom_metadata, accumulate: true) + @custom_metadata {:version, "1.0.0"} + @custom_metadata {:author, "Anonymous"} + + koan "Custom attributes can be registered and accumulated" do + metadata = Module.get_attribute(__MODULE__, :custom_metadata) + version_tuple = Enum.find(metadata, fn {key, _} -> key == :version end) + assert version_tuple == ___ + end + + # Attribute access in guards + @min_age 18 + + @doc "Checks if a person is an adult" + @spec adult?(integer()) :: boolean() + def adult?(age) when age >= @min_age, do: true + def adult?(_), do: false + + koan "Attributes can be used in guard expressions" do + assert adult?(25) == ___ + assert adult?(16) == ___ + end + + # External file reading at compile time + # This tells the compiler to recompile if README.md changes + @external_resource "README.md" + # @version File.read!("VERSION") |> String.trim() # Would read version from file + + # Since we don't have these files, let's simulate: + @version "1.2.3" + + koan "Attributes can read external files at compile time" do + assert @version == ___ + end + + # Conditional compilation + @compile_env Mix.env() + + if @compile_env == :dev do + @doc "This function only exists in development" + def debug_info, do: "Development mode: #{@compile_env}" + end + + koan "Attributes enable conditional compilation" do + # This will depend on the compilation environment + assert @compile_env in [:dev, :test, :prod] == ___ + end + + # Behaviour callbacks documentation + @doc """ + This would be a callback definition if we were defining a behaviour. + Behaviours use @callback to define the functions that must be implemented. + """ + # @callback handle_event(event :: any(), state :: any()) :: {:ok, any()} | {:error, String.t()} + + # Module attribute for configuration + @dialyzer {:no_return, deprecated_function: 0} + + # This hides the function from documentation + @doc false + def deprecated_function do + raise "This function is deprecated" + end + + koan "The module attribute @doc false hides functions from generated documentation" do + # The function exists but won't appear in docs + assert function_exported?(__MODULE__, :deprecated_function, 0) == ___ + end + + # Attribute computed from other attributes + @base_url "https://api.example.com" + @api_version "v1" + @full_url "#{@base_url}/#{@api_version}" + + koan "Attributes can be computed from other attributes" do + assert @full_url == ___ + end + + # Using attributes for code generation + @fields [:name, :email, :age] + + Enum.each(@fields, fn field -> + def unquote(:"get_#{field}")(user) do + Map.get(user, unquote(field)) + end + end) + + koan "Attributes can drive code generation with macros" do + user = %{name: "Bob", email: "bob@example.com", age: 35} + assert get_name(user) == ___ + assert get_email(user) == ___ + assert get_age(user) == ___ + end + + # Storing complex data structures + @lookup_table %{ + :red => "#FF0000", + :green => "#00FF00", + :blue => "#0000FF" + } + + @doc "Converts color names to hex codes" + @spec color_to_hex(atom()) :: String.t() | nil + def color_to_hex(color) do + Map.get(@lookup_table, color) + end + + koan "Attributes can store complex data structures" do + assert color_to_hex(:red) == ___ + assert color_to_hex(:purple) == ___ + end + + # Multiple type specs for the same function + @doc "Processes different types of input" + @spec process_input(String.t()) :: String.t() + @spec process_input(integer()) :: integer() + @spec process_input(list()) :: list() + def process_input(input) when is_binary(input), do: String.upcase(input) + def process_input(input) when is_integer(input), do: input * 2 + def process_input(input) when is_list(input), do: Enum.reverse(input) + + koan "Functions can have multiple type specifications" do + assert process_input("hello") == ___ + assert process_input(5) == ___ + assert process_input([1, 2, 3]) == ___ + end +end diff --git a/lib/koans/27_io_and_files.ex b/lib/koans/27_io_and_files.ex new file mode 100644 index 00000000..a1b53707 --- /dev/null +++ b/lib/koans/27_io_and_files.ex @@ -0,0 +1,311 @@ +defmodule IOAndFiles do + @moduledoc false + use Koans + + @intro "IO and Files - Reading, writing, and interacting with the outside world" + + koan "IO.puts writes to standard output" do + # We can't easily test stdout, but we can test the return value + result = IO.puts("Hello, World!") + assert result == ___ + end + + koan "IO.inspect returns its input while printing it" do + value = [1, 2, 3] + result = IO.inspect(value) + assert result == ___ + end + + koan "IO.inspect can be customized with options" do + data = %{name: "Alice", details: %{age: 30, city: "Boston"}} + result = IO.inspect(data, label: "User Data", pretty: true) + assert result == ___ + end + + koan "File.read/1 reads entire files" do + # Let's create a temporary file for testing + content = "Hello from file!" + File.write!("/tmp/test_koan.txt", content) + + result = File.read("/tmp/test_koan.txt") + assert result == ___ + + # Clean up + File.rm("/tmp/test_koan.txt") + end + + koan "File.read/1 returns error tuples for missing files" do + result = File.read("/tmp/nonexistent_file.txt") + assert elem(result, 0) == ___ + end + + koan "File.read!/1 raises exceptions for errors" do + assert_raise File.Error, fn -> + File.read!("/tmp/___") + end + end + + koan "File.write/2 creates and writes to files" do + path = "/tmp/write_test.txt" + content = "This is test content" + + result = File.write(path, content) + assert result == ___ + + # Verify it was written + {:ok, read_content} = File.read(path) + assert read_content == ___ + + File.rm(path) + end + + koan "File operations can be chained for processing" do + path = "/tmp/chain_test.txt" + original = "hello world" + + result = + path + |> File.write(original) + |> case do + :ok -> File.read(path) + error -> error + end + |> case do + {:ok, content} -> String.upcase(content) + error -> error + end + + assert result == ___ + File.rm(path) + end + + koan "File.exists?/1 checks if files exist" do + path = "/tmp/existence_test.txt" + + assert File.exists?(path) == ___ + + File.write!(path, "content") + assert File.exists?(path) == ___ + + File.rm!(path) + assert File.exists?(path) == ___ + end + + koan "File.ls/1 lists directory contents" do + # Create a test directory with some files + dir = "/tmp/test_dir_koan" + File.mkdir_p!(dir) + File.write!("#{dir}/file1.txt", "content1") + File.write!("#{dir}/file2.txt", "content2") + + {:ok, files} = File.ls(dir) + sorted_files = Enum.sort(files) + + assert sorted_files == ___ + + # Clean up + File.rm_rf!(dir) + end + + koan "Path module helps with file path operations" do + path = Path.join(["/", "home", "user", "documents"]) + assert path == ___ + + basename = Path.basename("/home/user/file.txt") + assert basename == ___ + + dirname = Path.dirname("/home/user/file.txt") + assert dirname == ___ + + extension = Path.extname("document.pdf") + assert extension == ___ + end + + koan "File.stream! creates lazy streams for large files" do + path = "/tmp/stream_test.txt" + content = "line 1\nline 2\nline 3\n" + File.write!(path, content) + + line_count = + path + |> File.stream!() + |> Enum.count() + + assert line_count == ___ + + first_line = + path + |> File.stream!() + |> Enum.take(1) + |> List.first() + |> String.trim() + + assert first_line == ___ + + File.rm!(path) + end + + koan "IO.StringIO creates in-memory IO devices" do + {:ok, string_io} = StringIO.open("initial content") + + # Read from it + content = IO.read(string_io, :all) + assert content == ___ + + # Write to it + IO.write(string_io, " added content") + + # Get the full content + {_input, output} = StringIO.contents(string_io) + assert output == ___ + + StringIO.close(string_io) + end + + koan "File.cp/2 and File.mv/2 copy and move files" do + source = "/tmp/source.txt" + copy_dest = "/tmp/copy.txt" + move_dest = "/tmp/moved.txt" + + File.write!(source, "original content") + + # Copy file + result = File.cp(source, copy_dest) + assert result == ___ + assert File.read!(copy_dest) == ___ + + # Move file + result = File.mv(copy_dest, move_dest) + assert result == ___ + assert File.exists?(copy_dest) == ___ + assert File.exists?(move_dest) == ___ + + # Clean up + File.rm!(source) + File.rm!(move_dest) + end + + koan "File.stat/1 provides file information" do + path = "/tmp/stat_test.txt" + File.write!(path, "some content for stat testing") + + {:ok, stat} = File.stat(path) + + assert stat.type == ___ + assert stat.size > 0 == ___ + assert is_integer(stat.mtime) == ___ + + File.rm!(path) + end + + koan "File operations handle directory creation" do + dir_path = "/tmp/nested/deep/directory" + + # mkdir_p creates parent directories + result = File.mkdir_p(dir_path) + assert result == ___ + assert File.dir?(dir_path) == ___ + + # Regular mkdir fails if parents don't exist + another_nested = "/tmp/another/nested" + result = File.mkdir(another_nested) + assert elem(result, 0) == ___ + + # Clean up + File.rm_rf!("/tmp/nested") + end + + koan "IO.getn prompts for user input" do + # We can't easily test interactive input, but we can test with StringIO + {:ok, input_device} = StringIO.open("test input") + + result = IO.getn(input_device, "Enter text: ", 4) + assert result == ___ + + StringIO.close(input_device) + end + + koan "File.open/2 provides more control over file operations" do + path = "/tmp/open_test.txt" + + # Open file for writing + {:ok, file} = File.open(path, [:write]) + IO.write(file, "Written with File.open") + File.close(file) + + # Open file for reading + {:ok, file} = File.open(path, [:read]) + content = IO.read(file, :all) + File.close(file) + + assert content == ___ + + File.rm!(path) + end + + koan "File operations can work with binary data" do + path = "/tmp/binary_test.bin" + binary_data = <<1, 2, 3, 4, 255>> + + File.write!(path, binary_data) + read_data = File.read!(path) + + assert read_data == ___ + assert byte_size(read_data) == ___ + + File.rm!(path) + end + + koan "Temporary files can be created safely" do + # Create a temporary file using our helper + path = temp_path() + File.write!(path, "temporary content") + + assert File.exists?(path) == ___ + content = File.read!(path) + assert content == ___ + + File.rm!(path) + end + + # Helper function to create temp paths since we don't have Temp module + defp temp_path do + "/tmp/koan_temp_#{:rand.uniform(10000)}.txt" + end + + koan "File.touch/1 creates empty files or updates timestamps" do + path = temp_path() + + # Create file + result = File.touch(path) + assert result == ___ + assert File.exists?(path) == ___ + + # File should be empty + content = File.read!(path) + assert content == ___ + + File.rm!(path) + end + + koan "Working with CSV-like data using File.stream!" do + path = "/tmp/csv_test.csv" + csv_content = "name,age,city\nAlice,30,Boston\nBob,25,Seattle\nCharlie,35,Austin" + File.write!(path, csv_content) + + parsed_data = + path + |> File.stream!() + # Skip header + |> Stream.drop(1) + |> Stream.map(&String.trim/1) + |> Stream.map(&String.split(&1, ",")) + |> Enum.to_list() + + first_record = List.first(parsed_data) + assert first_record == ___ + assert length(parsed_data) == ___ + + File.rm!(path) + end +end diff --git a/lib/koans/28_otp_behaviors.ex b/lib/koans/28_otp_behaviors.ex new file mode 100644 index 00000000..3bbef4b5 --- /dev/null +++ b/lib/koans/28_otp_behaviors.ex @@ -0,0 +1,373 @@ +defmodule OTPBehaviors do + @moduledoc false + use Koans + + @intro "OTP Behaviors - Building robust, fault-tolerant systems" + + # Define a custom behavior + defmodule EventHandler do + @moduledoc "A simple behavior for handling events" + + @doc "Handle an incoming event" + @callback handle_event(event :: any(), state :: any()) :: {:ok, any()} | {:error, any()} + + @doc "Initialize the handler" + @callback init(args :: any()) :: {:ok, any()} | {:error, any()} + end + + # Implement the behavior + defmodule LoggingHandler do + @behaviour EventHandler + + def init(log_level) do + {:ok, %{log_level: log_level, events: []}} + end + + def handle_event(event, state) do + new_events = [event | state.events] + {:ok, %{state | events: new_events}} + end + end + + koan "Behaviors define contracts that modules must implement" do + {:ok, state} = LoggingHandler.init(:info) + assert state.log_level == ___ + assert state.events == ___ + end + + koan "Behavior implementations must provide all required callbacks" do + {:ok, state} = LoggingHandler.init(:debug) + {:ok, new_state} = LoggingHandler.handle_event("test event", state) + + assert length(new_state.events) == ___ + assert List.first(new_state.events) == ___ + end + + # Simple Supervisor example + defmodule SimpleSupervisor do + use Supervisor + + def start_link(init_args) do + Supervisor.start_link(__MODULE__, init_args, name: __MODULE__) + end + + def init(_init_args) do + children = [ + {SimpleWorker, %{name: "worker1"}}, + {SimpleWorker, %{name: "worker2"}} + ] + + Supervisor.init(children, strategy: :one_for_one) + end + end + + defmodule SimpleWorker do + use GenServer + + def start_link(args) do + GenServer.start_link(__MODULE__, args) + end + + def init(args) do + {:ok, args} + end + + def handle_call(:get_state, _from, state) do + {:reply, state, state} + end + end + + koan "Supervisors manage child processes" do + {:ok, supervisor_pid} = SimpleSupervisor.start_link([]) + children = Supervisor.which_children(supervisor_pid) + + assert length(children) == ___ + + Supervisor.stop(supervisor_pid) + end + + # Application behavior example + defmodule SampleApp do + use Application + + def start(_type, _args) do + children = [ + {Registry, keys: :unique, name: MyRegistry}, + {DynamicSupervisor, name: MyDynamicSupervisor, strategy: :one_for_one} + ] + + opts = [strategy: :one_for_one, name: SampleApp.Supervisor] + Supervisor.start_link(children, opts) + end + + def stop(_state) do + :ok + end + end + + koan "Applications define how to start and stop supervision trees" do + # We can't easily start the full application, but we can check the structure + assert function_exported?(SampleApp, :start, 2) == ___ + assert function_exported?(SampleApp, :stop, 1) == ___ + end + + # Custom GenServer with specific behavior patterns + defmodule Counter do + use GenServer + + # Client API + def start_link(initial_value \\ 0) do + GenServer.start_link(__MODULE__, initial_value, name: __MODULE__) + end + + def increment do + GenServer.call(__MODULE__, :increment) + end + + def decrement do + GenServer.call(__MODULE__, :decrement) + end + + def get_value do + GenServer.call(__MODULE__, :get_value) + end + + def reset do + GenServer.cast(__MODULE__, :reset) + end + + # Server callbacks + def init(initial_value) do + {:ok, initial_value} + end + + def handle_call(:increment, _from, state) do + new_state = state + 1 + {:reply, new_state, new_state} + end + + def handle_call(:decrement, _from, state) do + new_state = state - 1 + {:reply, new_state, new_state} + end + + def handle_call(:get_value, _from, state) do + {:reply, state, state} + end + + def handle_cast(:reset, _state) do + {:noreply, 0} + end + + def terminate(reason, state) do + IO.puts("Counter terminating: #{inspect(reason)}, final state: #{state}") + :ok + end + end + + koan "GenServers provide structured client-server patterns" do + {:ok, _pid} = Counter.start_link(5) + + assert Counter.get_value() == ___ + assert Counter.increment() == ___ + assert Counter.increment() == ___ + assert Counter.decrement() == ___ + + Counter.reset() + assert Counter.get_value() == ___ + + GenServer.stop(Counter) + end + + # Task Supervisor for managing dynamic tasks + defmodule TaskManager do + use DynamicSupervisor + + def start_link(_args) do + DynamicSupervisor.start_link(__MODULE__, :ok, name: __MODULE__) + end + + def init(:ok) do + DynamicSupervisor.init(strategy: :one_for_one) + end + + def start_task(fun) do + spec = Task.child_spec(fun) + DynamicSupervisor.start_child(__MODULE__, spec) + end + + def list_tasks do + DynamicSupervisor.which_children(__MODULE__) + end + end + + koan "DynamicSupervisor manages children that are started and stopped dynamically" do + {:ok, _pid} = TaskManager.start_link([]) + + # Start a task + {:ok, task_pid} = + TaskManager.start_task(fn -> + Process.sleep(1000) + :completed + end) + + tasks = TaskManager.list_tasks() + assert length(tasks) == ___ + + # Task should be running + assert Process.alive?(task_pid) == ___ + + DynamicSupervisor.stop(TaskManager) + end + + # Registry for process discovery + defmodule ServiceRegistry do + def start_link do + Registry.start_link(keys: :unique, name: __MODULE__) + end + + def register_service(name, pid) do + Registry.register(__MODULE__, name, pid) + end + + def find_service(name) do + case Registry.lookup(__MODULE__, name) do + [{pid, _}] -> {:ok, pid} + [] -> {:error, :not_found} + end + end + + def list_services do + Registry.select(__MODULE__, [{{:"$1", :"$2", :"$3"}, [], [{{:"$1", :"$2"}}]}]) + end + end + + koan "Registry provides service discovery for processes" do + {:ok, _registry_pid} = ServiceRegistry.start_link() + {:ok, worker_pid} = SimpleWorker.start_link(%{name: "test_service"}) + + # Register the service + {:ok, _} = ServiceRegistry.register_service(:test_service, worker_pid) + + # Find the service + {:ok, found_pid} = ServiceRegistry.find_service(:test_service) + assert found_pid == ___ + + # List all services + services = ServiceRegistry.list_services() + assert length(services) == ___ + + GenServer.stop(worker_pid) + Registry.stop(ServiceRegistry) + end + + # Custom behavior with optional callbacks + defmodule Worker do + @doc "Define the behavior for worker modules" + + @callback start_work(args :: any()) :: {:ok, any()} | {:error, any()} + @callback stop_work(state :: any()) :: :ok + + @optional_callbacks stop_work: 1 + + defmacro __using__(_opts) do + quote do + @behaviour Worker + + # Provide default implementation for optional callback + def stop_work(_state), do: :ok + + defoverridable stop_work: 1 + end + end + end + + defmodule DatabaseWorker do + use Worker + + def start_work(config) do + {:ok, %{connected: true, config: config}} + end + + # Override the default implementation + def stop_work(state) do + IO.puts("Closing database connection") + :ok + end + end + + defmodule SimpleWorkerImpl do + use Worker + + def start_work(args) do + {:ok, %{status: :working, args: args}} + end + + # Uses default stop_work implementation + end + + koan "Behaviors can have optional callbacks with default implementations" do + {:ok, db_state} = DatabaseWorker.start_work(%{host: "localhost"}) + assert db_state.connected == ___ + + {:ok, simple_state} = SimpleWorkerImpl.start_work("test") + assert simple_state.status == ___ + + # Both should implement stop_work + assert DatabaseWorker.stop_work(db_state) == ___ + assert SimpleWorkerImpl.stop_work(simple_state) == ___ + end + + # Supervision strategies demonstration + defmodule RestartStrategies do + def demonstrate_one_for_one do + # In :one_for_one, only the failed child is restarted + children = [ + {SimpleWorker, %{name: "worker1"}}, + {SimpleWorker, %{name: "worker2"}}, + {SimpleWorker, %{name: "worker3"}} + ] + + {:ok, supervisor} = Supervisor.start_link(children, strategy: :one_for_one) + initial_count = length(Supervisor.which_children(supervisor)) + + Supervisor.stop(supervisor) + initial_count + end + + def demonstrate_one_for_all do + # In :one_for_all, if one child dies, all children are restarted + children = [ + {SimpleWorker, %{name: "worker1"}}, + {SimpleWorker, %{name: "worker2"}} + ] + + {:ok, supervisor} = Supervisor.start_link(children, strategy: :one_for_all) + initial_count = length(Supervisor.which_children(supervisor)) + + Supervisor.stop(supervisor) + initial_count + end + end + + koan "Different supervision strategies handle failures differently" do + one_for_one_count = RestartStrategies.demonstrate_one_for_one() + assert one_for_one_count == ___ + + one_for_all_count = RestartStrategies.demonstrate_one_for_all() + assert one_for_all_count == ___ + end + + koan "OTP provides building blocks for fault-tolerant systems" do + # The key principles of OTP + principles = [ + :let_it_crash, + :supervision_trees, + :isolation, + :restart_strategies + ] + + assert :let_it_crash in principles == ___ + assert length(principles) == ___ + end +end diff --git a/test/koans/control_flow_koans_test.exs b/test/koans/control_flow_koans_test.exs new file mode 100644 index 00000000..c4ac8fc9 --- /dev/null +++ b/test/koans/control_flow_koans_test.exs @@ -0,0 +1,24 @@ +defmodule ControlFlowTests do + use ExUnit.Case + import TestHarness + + test "Control Flow" do + answers = [ + "yes", + "math works", + "will execute", + {:multiple, ["falsy", "falsy", "truthy", "truthy", "truthy"]}, + "matched with x = 2", + {:multiple, ["positive", "zero", "negative"]}, + {:multiple, ["empty", "one element", "two elements", "many elements"]}, + "warm", + {:multiple, [{:ok, 5}, {:error, "division by zero"}]}, + {:multiple, ["Success: Hello", "Client error: 404", "Request failed: timeout"]}, + {:multiple, + ["positive even integer", "positive odd integer", "negative integer", "float", "other"]}, + "verified active user" + ] + + test_all(ControlFlow, answers) + end +end diff --git a/test/koans/enum_koans_test.exs b/test/koans/enum_koans_test.exs index cdfea9eb..5fbab949 100644 --- a/test/koans/enum_koans_test.exs +++ b/test/koans/enum_koans_test.exs @@ -5,7 +5,9 @@ defmodule EnumTests do test "Enums" do answers = [ 3, - 2, + 3, + 1, + {:multiple, [2, ArgumentError]}, {:multiple, [true, false]}, {:multiple, [true, false]}, {:multiple, [true, false]}, @@ -19,7 +21,11 @@ defmodule EnumTests do 2, nil, :no_such_element, - 6 + 6, + {:multiple, [[[1, 2], [3, 4], [5, 6]], [[1, 2, 3], [4, 5]]]}, + [1, 10, 2, 20, 3, 30], + {:multiple, [["apple", "apricot"], ["banana", "blueberry"]]}, + [4, 8, 12] ] test_all(Enums, answers) diff --git a/test/koans/error_handling_koans_test.exs b/test/koans/error_handling_koans_test.exs new file mode 100644 index 00000000..30dc7478 --- /dev/null +++ b/test/koans/error_handling_koans_test.exs @@ -0,0 +1,36 @@ +defmodule ErrorHandlingTests do + use ExUnit.Case + import TestHarness + + test "Error Handling" do + answers = [ + {:multiple, [{:ok, 123}, {:error, :invalid_format}]}, + "Result: 5.0", + "Cannot divide by zero!", + {:multiple, [{:ok, 2}, {:error, :invalid_argument}, {:error, "abc is not a list"}]}, + {:multiple, + [ + {:error, :arithmetic}, + {:error, :missing_key}, + {:error, :invalid_argument}, + {:ok, "success"} + ]}, + "caught thrown value", + :returned_value, + {:multiple, [:success, "it worked"]}, + "caught custom error: custom failure", + "key not found", + "caught normal exit", + {:multiple, + [ + {:error, {:exception, "connection failed"}}, + {:error, :timeout}, + {:error, :invalid_query}, + {:ok, "data retrieved"} + ]}, + {:multiple, [:conversion_error, "user input processing"]} + ] + + test_all(ErrorHandling, answers) + end +end diff --git a/test/koans/functions_koans_test.exs b/test/koans/functions_koans_test.exs index 4e504ad1..2ad0bf70 100644 --- a/test/koans/functions_koans_test.exs +++ b/test/koans/functions_koans_test.exs @@ -19,7 +19,9 @@ defmodule FunctionsTests do 100, 1000, "Full Name", - {:multiple, ["GOOD", "good"]} + {:multiple, [24, "hello_world"]}, + {:multiple, ["GOOD", "good"]}, + {:multiple, [12, 5]} ] test_all(Functions, answers) diff --git a/test/koans/genservers_koans_test.exs b/test/koans/genservers_koans_test.exs index 3a6569e2..619c6cd2 100644 --- a/test/koans/genservers_koans_test.exs +++ b/test/koans/genservers_koans_test.exs @@ -12,7 +12,9 @@ defmodule GenServersTests do {:error, "Incorrect password!"}, "Congrats! Your process was successfully named.", {:ok, "Laptop unlocked!"}, - {:multiple, ["Laptop unlocked!", "Incorrect password!", "Jack Sparrow"]} + {:multiple, ["Laptop unlocked!", "Incorrect password!", "Jack Sparrow"]}, + 1, + {:multiple, ["the state", "the state"]} ] test_all(GenServers, answers) diff --git a/test/koans/patterns_koans_test.exs b/test/koans/patterns_koans_test.exs index 2d9fa364..8286f585 100644 --- a/test/koans/patterns_koans_test.exs +++ b/test/koans/patterns_koans_test.exs @@ -15,7 +15,7 @@ defmodule PatternsTests do [1, 2, 3], {:multiple, ["Meow", "Woof", "Eh?"]}, {:multiple, ["Mickey", "Donald", "I need a name!"]}, - "dog", + "barking", "Max", {:multiple, [true, false]}, "Max", @@ -23,7 +23,10 @@ defmodule PatternsTests do 2, {:multiple, ["The number One", "The number Two", "The number 3"]}, "same", - 2 + 2, + {:multiple, [30, "dark"]}, + {:multiple, [1, 2, [3, 4, 5], 1]}, + {:multiple, [5, :division_by_zero]} ] test_all(PatternMatching, answers) diff --git a/test/koans/pipe_operator_koans_test.exs b/test/koans/pipe_operator_koans_test.exs new file mode 100644 index 00000000..2ab34c10 --- /dev/null +++ b/test/koans/pipe_operator_koans_test.exs @@ -0,0 +1,27 @@ +defmodule PipeOperatorTests do + use ExUnit.Case + import TestHarness + + test "Pipe Operator" do + answers = [ + "HELLO-WORLD", + "hello_world", + [6, 8, 10], + "hello, world", + 20, + "1-2-3", + ["Alice", "Charlie"], + ["QUICK", "BROWN", "JUMPS"], + [a: 2, b: 4, c: 6], + {:multiple, [{:ok, 84}, {:error, :invalid_number}]}, + {:multiple, [["HELLO", "WORLD"], ["hello", "world"]]}, + 250, + 12, + {:multiple, [2, 2, 1]}, + 5, + {:multiple, ["Result: 5.0", "Error: division_by_zero"]} + ] + + test_all(PipeOperator, answers) + end +end diff --git a/test/koans/with_statement_koans_test.exs b/test/koans/with_statement_koans_test.exs new file mode 100644 index 00000000..4c4cd36e --- /dev/null +++ b/test/koans/with_statement_koans_test.exs @@ -0,0 +1,26 @@ +defmodule WithStatementTests do + use ExUnit.Case + import TestHarness + + test "With Statement" do + answers = [ + {:multiple, [9, {:error, :invalid_number}]}, + {:multiple, [{:ok, "Adult user: Alice"}, {:error, :underage}, {:error, :missing_data}]}, + {:multiple, [{:ok, 2}, {:error, :division_by_zero}, {:error, :negative_sqrt}]}, + {:multiple, + [{:ok, "user@example.com"}, {:error, :invalid_email}, {:error, :missing_email}]}, + {:multiple, [{:ok, 5}, {:error, :not_positive}, {:ok, 150}, {:error, :not_a_number}]}, + {:ok, %{id: 1}}, + {:multiple, [8, nil]}, + :TODO, + {:multiple, + [ + {:ok, "step3_valid"}, + {:error, "Failed at step 1: TODO"}, + {:error, "Failed at step 2: TODO"} + ]} + ] + + test_all(WithStatement, answers) + end +end