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