|  | 
|  | 1 | +defmodule WithStatement do | 
|  | 2 | +  @moduledoc false | 
|  | 3 | +  use Koans | 
|  | 4 | + | 
|  | 5 | +  @intro "The With Statement - Elegant error handling and happy path programming" | 
|  | 6 | + | 
|  | 7 | +  koan "With lets you chain operations that might fail" do | 
|  | 8 | +    parse_and_add = fn str1, str2 -> | 
|  | 9 | +      with {a, ""} <- Integer.parse(str1), | 
|  | 10 | +           {b, ""} <- Integer.parse(str2) do | 
|  | 11 | +        {:ok, a + b} | 
|  | 12 | +      else | 
|  | 13 | +        :error -> {:error, :invalid_number} | 
|  | 14 | +      end | 
|  | 15 | +    end | 
|  | 16 | + | 
|  | 17 | +    assert parse_and_add.("5", "4") == ___ | 
|  | 18 | +    assert parse_and_add.("abc", "1") == ___ | 
|  | 19 | +  end | 
|  | 20 | + | 
|  | 21 | +  koan "With short-circuits on the first non-matching pattern" do | 
|  | 22 | +    process_user = fn user_data -> | 
|  | 23 | +      with {:ok, name} <- Map.fetch(user_data, :name), | 
|  | 24 | +           {:ok, age} <- Map.fetch(user_data, :age), | 
|  | 25 | +           true <- age >= 18 do | 
|  | 26 | +        {:ok, "Adult user: #{name}"} | 
|  | 27 | +      else | 
|  | 28 | +        :error -> {:error, :missing_data} | 
|  | 29 | +        false -> {:error, :underage} | 
|  | 30 | +      end | 
|  | 31 | +    end | 
|  | 32 | + | 
|  | 33 | +    assert process_user.(%{name: "Alice", age: 25}) == ___ | 
|  | 34 | +    assert process_user.(%{name: "Bob", age: 16}) == ___ | 
|  | 35 | +    assert process_user.(%{age: 25}) == ___ | 
|  | 36 | +  end | 
|  | 37 | + | 
|  | 38 | +  defp safe_divide(_, 0), do: {:error, :division_by_zero} | 
|  | 39 | +  defp safe_divide(x, y), do: {:ok, x / y} | 
|  | 40 | + | 
|  | 41 | +  defp safe_sqrt(x) when x < 0, do: {:error, :negative_sqrt} | 
|  | 42 | +  defp safe_sqrt(x), do: {:ok, :math.sqrt(x)} | 
|  | 43 | + | 
|  | 44 | +  koan "With can handle multiple different error patterns" do | 
|  | 45 | +    divide_and_sqrt = fn x, y -> | 
|  | 46 | +      with {:ok, division} <- safe_divide(x, y), | 
|  | 47 | +           {:ok, sqrt} <- safe_sqrt(division) do | 
|  | 48 | +        {:ok, sqrt} | 
|  | 49 | +      else | 
|  | 50 | +        {:error, :division_by_zero} -> {:error, "Cannot divide by zero"} | 
|  | 51 | +        {:error, :negative_sqrt} -> {:error, "Cannot take square root of negative number"} | 
|  | 52 | +      end | 
|  | 53 | +    end | 
|  | 54 | + | 
|  | 55 | +    assert divide_and_sqrt.(16, 4) == ___ | 
|  | 56 | +    assert divide_and_sqrt.(10, 0) == ___ | 
|  | 57 | +    assert divide_and_sqrt.(-16, 4) == ___ | 
|  | 58 | +  end | 
|  | 59 | + | 
|  | 60 | +  koan "With works great for nested data extraction" do | 
|  | 61 | +    get_user_email = fn data -> | 
|  | 62 | +      with {:ok, user} <- Map.fetch(data, :user), | 
|  | 63 | +           {:ok, profile} <- Map.fetch(user, :profile), | 
|  | 64 | +           {:ok, email} <- Map.fetch(profile, :email), | 
|  | 65 | +           true <- String.contains?(email, "@") do | 
|  | 66 | +        {:ok, email} | 
|  | 67 | +      else | 
|  | 68 | +        :error -> {:error, :missing_data} | 
|  | 69 | +        false -> {:error, :invalid_email} | 
|  | 70 | +      end | 
|  | 71 | +    end | 
|  | 72 | + | 
|  | 73 | +    valid_data = %{ | 
|  | 74 | +      user: %{ | 
|  | 75 | +        profile: %{ | 
|  | 76 | + | 
|  | 77 | +        } | 
|  | 78 | +      } | 
|  | 79 | +    } | 
|  | 80 | + | 
|  | 81 | +    invalid_email_data = %{ | 
|  | 82 | +      user: %{ | 
|  | 83 | +        profile: %{ | 
|  | 84 | +          email: "notanemail" | 
|  | 85 | +        } | 
|  | 86 | +      } | 
|  | 87 | +    } | 
|  | 88 | + | 
|  | 89 | +    assert get_user_email.(valid_data) == ___ | 
|  | 90 | +    assert get_user_email.(invalid_email_data) == ___ | 
|  | 91 | +    assert get_user_email.(%{}) == ___ | 
|  | 92 | +  end | 
|  | 93 | + | 
|  | 94 | +  koan "With can combine pattern matching with guards" do | 
|  | 95 | +    process_number = fn input -> | 
|  | 96 | +      with {num, ""} <- Integer.parse(input), | 
|  | 97 | +           true <- num > 0, | 
|  | 98 | +           result when result < 1000 <- num * 10 do | 
|  | 99 | +        {:ok, result} | 
|  | 100 | +      else | 
|  | 101 | +        :error -> {:error, :not_a_number} | 
|  | 102 | +        false -> {:error, :not_positive} | 
|  | 103 | +        result when result >= 100 -> {:error, :result_too_large} | 
|  | 104 | +      end | 
|  | 105 | +    end | 
|  | 106 | + | 
|  | 107 | +    assert process_number.("5") == ___ | 
|  | 108 | +    assert process_number.("-5") == ___ | 
|  | 109 | +    assert process_number.("150") == ___ | 
|  | 110 | +    assert process_number.("abc") == ___ | 
|  | 111 | +  end | 
|  | 112 | + | 
|  | 113 | +  koan "With clauses can have side effects and assignments" do | 
|  | 114 | +    register_user = fn user_data -> | 
|  | 115 | +      with {:ok, email} <- validate_email(user_data[:email]), | 
|  | 116 | +           {:ok, password} <- validate_password(user_data[:password]), | 
|  | 117 | +           hashed_password = hash_password(password), | 
|  | 118 | +           {:ok, user} <- save_user(email, hashed_password) do | 
|  | 119 | +        {:ok, user} | 
|  | 120 | +      else | 
|  | 121 | +        {:error, reason} -> {:error, reason} | 
|  | 122 | +      end | 
|  | 123 | +    end | 
|  | 124 | + | 
|  | 125 | +    user_data = %{email: "[email protected]", password: "secure123"} | 
|  | 126 | +    assert ___ = register_user.(user_data) | 
|  | 127 | +  end | 
|  | 128 | + | 
|  | 129 | +  defp validate_email(email) when is_binary(email) and byte_size(email) > 0 do | 
|  | 130 | +    if String.contains?(email, "@"), do: {:ok, email}, else: {:error, :invalid_email} | 
|  | 131 | +  end | 
|  | 132 | + | 
|  | 133 | +  defp validate_email(_), do: {:error, :invalid_email} | 
|  | 134 | + | 
|  | 135 | +  defp validate_password(password) when is_binary(password) and byte_size(password) >= 6 do | 
|  | 136 | +    {:ok, password} | 
|  | 137 | +  end | 
|  | 138 | + | 
|  | 139 | +  defp validate_password(_), do: {:error, :weak_password} | 
|  | 140 | + | 
|  | 141 | +  defp hash_password(password), do: "hashed_" <> password | 
|  | 142 | + | 
|  | 143 | +  defp save_user(email, hashed_password) do | 
|  | 144 | +    {:ok, %{id: 1, email: email, password: hashed_password}} | 
|  | 145 | +  end | 
|  | 146 | + | 
|  | 147 | +  koan "With can be used without an else clause for simpler cases" do | 
|  | 148 | +    simple_calculation = fn x, y -> | 
|  | 149 | +      with num1 when is_number(num1) <- x, | 
|  | 150 | +           num2 when is_number(num2) <- y do | 
|  | 151 | +        num1 + num2 | 
|  | 152 | +      end | 
|  | 153 | +    end | 
|  | 154 | + | 
|  | 155 | +    assert simple_calculation.(5, 3) == ___ | 
|  | 156 | +    # When pattern doesn't match and no else, returns the non-matching value | 
|  | 157 | +    assert simple_calculation.("5", 3) == ___ | 
|  | 158 | +  end | 
|  | 159 | + | 
|  | 160 | +  koan "With can handle complex nested error scenarios" do | 
|  | 161 | +    complex_workflow = fn data -> | 
|  | 162 | +      with {:ok, step1} <- step_one(data), | 
|  | 163 | +           {:ok, step2} <- step_two(step1), | 
|  | 164 | +           {:ok, step3} <- step_three(step2) do | 
|  | 165 | +        {:ok, step3} | 
|  | 166 | +      else | 
|  | 167 | +        {:error, :step1_failed} -> {:error, "Failed at step 1: invalid input"} | 
|  | 168 | +        {:error, :step2_failed} -> {:error, "Failed at step 2: processing error"} | 
|  | 169 | +        {:error, :step3_failed} -> {:error, "Failed at step 3: final validation error"} | 
|  | 170 | +        other -> {:error, "Unexpected error: #{inspect(other)}"} | 
|  | 171 | +      end | 
|  | 172 | +    end | 
|  | 173 | + | 
|  | 174 | +    assert complex_workflow.("valid") == ___ | 
|  | 175 | +    assert complex_workflow.("step1_fail") == ___ | 
|  | 176 | +    assert complex_workflow.("step2_fail") == ___ | 
|  | 177 | +  end | 
|  | 178 | + | 
|  | 179 | +  defp step_one("step1_fail"), do: {:error, :step1_failed} | 
|  | 180 | +  defp step_one(data), do: {:ok, "step1_" <> data} | 
|  | 181 | + | 
|  | 182 | +  defp step_two("step1_step2_fail"), do: {:error, :step2_failed} | 
|  | 183 | +  defp step_two(data), do: {:ok, "step2_" <> data} | 
|  | 184 | + | 
|  | 185 | +  defp step_three("step2_step1_step3_fail"), do: {:error, :step3_failed} | 
|  | 186 | +  defp step_three(data), do: {:ok, "step3_" <> data} | 
|  | 187 | +end | 
0 commit comments