├── .dockerignore ├── .formatter.exs ├── .gitignore ├── config └── config.exs ├── docker.sh ├── test ├── support │ ├── passing_koan.ex │ ├── single_arity_koan.ex │ └── sample_koan.ex ├── runner_test.exs ├── koans │ ├── atoms_koans_test.exs │ ├── tasks_koans_test.exs │ ├── equalities_koan_test.exs │ ├── agents_koans_test.exs │ ├── keyword_lists_koans_test.exs │ ├── protocols_koans_test.exs │ ├── map_sets_koans_test.exs │ ├── tuples_koans_test.exs │ ├── sigils_koans_test.exs │ ├── strings_koan_test.exs │ ├── comprehensions_koans_test.exs │ ├── maps_koans_test.exs │ ├── structs_koans_test.exs │ ├── lists_koans_test.exs │ ├── processes_koans_test.exs │ ├── genservers_koans_test.exs │ ├── pipe_operator_koans_test.exs │ ├── numbers_koans_test.exs │ ├── enum_koans_test.exs │ ├── control_flow_koans_test.exs │ ├── functions_koans_test.exs │ ├── patterns_koans_test.exs │ ├── with_statement_koans_test.exs │ └── error_handling_koans_test.exs ├── display │ ├── intro_test.exs │ ├── notification_test.exs │ ├── progress_bar_test.exs │ └── failure_test.exs ├── executor_test.exs ├── test_helper.exs ├── tracker_test.exs └── blanks_test.exs ├── Dockerfile ├── lib ├── display │ ├── intro.ex │ ├── notification.ex │ ├── progress_bar.ex │ ├── colours.ex │ └── failure.ex ├── elixir_koans.ex ├── koans │ ├── 04_atoms.ex │ ├── 07_keyword_lists.ex │ ├── 01_equalities.ex │ ├── 11_sigils.ex │ ├── 05_tuples.ex │ ├── 20_comprehensions.ex │ ├── 02_strings.ex │ ├── 17_agents.ex │ ├── 16_tasks.ex │ ├── 19_protocols.ex │ ├── 08_maps.ex │ ├── 10_structs.ex │ ├── 06_lists.ex │ ├── 09_map_sets.ex │ ├── 03_numbers.ex │ ├── 14_enums.ex │ ├── 21_control_flow.ex │ ├── 13_functions.ex │ ├── 15_processes.ex │ ├── 12_pattern_matching.ex │ ├── 22_error_handling.ex │ ├── 23_pipe_operator.ex │ ├── 24_with_statement.ex │ └── 18_genservers.ex ├── meditate.ex ├── watcher.ex ├── blanks.ex ├── execute.ex ├── tracker.ex ├── display.ex ├── runner.ex └── koans.ex ├── CONTRIBUTORS.md ├── .travis.yml ├── mix.exs ├── mix.lock ├── LICENSE.md ├── .github └── workflows │ └── elixir.yml └── README.md /.dockerignore: -------------------------------------------------------------------------------- 1 | lib/koans 2 | _build 3 | -------------------------------------------------------------------------------- /.formatter.exs: -------------------------------------------------------------------------------- 1 | [ 2 | inputs: ["{config,lib,test}/**/*.{ex,exs}"] 3 | ] 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /_build 2 | /cover 3 | /deps 4 | erl_crash.dump 5 | *.ez 6 | .tool-versions 7 | -------------------------------------------------------------------------------- /config/config.exs: -------------------------------------------------------------------------------- 1 | import Config 2 | 3 | config :ex_unit, assert_receive_timeout: 100 4 | -------------------------------------------------------------------------------- /docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh -e 2 | 3 | docker build -t elixir-koans . 4 | docker run --rm -v `pwd`/lib/koans:/elixir-koans/lib/koans -ti elixir-koans 5 | -------------------------------------------------------------------------------- /test/support/passing_koan.ex: -------------------------------------------------------------------------------- 1 | defmodule PassingKoan do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "something" 6 | 7 | koan "Hi there" do 8 | assert 1 == 1 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM elixir:1.18.4 2 | RUN apt-get update && apt-get install -y inotify-tools 3 | WORKDIR /elixir-koans 4 | ADD . /elixir-koans/ 5 | RUN mix local.hex --force 6 | RUN mix deps.get 7 | CMD mix meditate 8 | -------------------------------------------------------------------------------- /test/runner_test.exs: -------------------------------------------------------------------------------- 1 | defmodule RunnerTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "path to number" do 5 | path = "lib/koans/01_just_an_example.ex" 6 | assert Runner.path_to_number(path) == 1 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /test/support/single_arity_koan.ex: -------------------------------------------------------------------------------- 1 | defmodule SingleArity do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro """ 6 | A koan with single arity testing 7 | """ 8 | 9 | koan "Only one" do 10 | assert match?(:foo, ___) 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /test/support/sample_koan.ex: -------------------------------------------------------------------------------- 1 | defmodule SampleKoan do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro """ 6 | There is something 7 | """ 8 | 9 | koan "Thinking more than once" do 10 | assert 3 == ___ 11 | assert 4 == ___ 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/koans/atoms_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AtomsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Atoms" do 6 | answers = [ 7 | :human, 8 | {:multiple, [true, true, true, false]}, 9 | {:multiple, [true, nil]} 10 | ] 11 | 12 | test_all(Atoms, answers) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /test/koans/tasks_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TasksTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Tasks" do 6 | answers = [ 7 | 10, 8 | :ok, 9 | nil, 10 | false, 11 | 9, 12 | [1, 4, 9, 16] 13 | ] 14 | 15 | test_all(Tasks, answers) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/koans/equalities_koan_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EqualitiesTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Equalities" do 6 | answers = [ 7 | true, 8 | false, 9 | 1, 10 | 2, 11 | 1, 12 | 4, 13 | 2 14 | ] 15 | 16 | test_all(Equalities, answers) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /test/display/intro_test.exs: -------------------------------------------------------------------------------- 1 | defmodule IntroTest do 2 | use ExUnit.Case 3 | 4 | alias Display.Intro 5 | 6 | test "module not visited yet" do 7 | assert Intro.intro(SampleKoan, []) == "There is something\n\n" 8 | end 9 | 10 | test "module has been visited" do 11 | assert Intro.intro(SampleKoan, [SampleKoan]) == "" 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /test/display/notification_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NotificationTest do 2 | use ExUnit.Case 3 | alias Display.Notifications 4 | 5 | test "shows possible koans when a koan can not be found" do 6 | message = Notifications.invalid_koan(SampleKoan, [PassingKoan]) 7 | assert message == "Did not find koan SampleKoan in PassingKoan" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /test/koans/agents_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule AgentTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Agents" do 6 | answers = [ 7 | "Hi there", 8 | "Why hello", 9 | "HI THERE", 10 | {:multiple, [["Milk"], ["Bread", "Milk"]]}, 11 | false 12 | ] 13 | 14 | test_all(Agents, answers) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/display/intro.ex: -------------------------------------------------------------------------------- 1 | defmodule Display.Intro do 2 | @moduledoc false 3 | alias Display.Paint 4 | 5 | def intro(module, modules) do 6 | if module in modules do 7 | "" 8 | else 9 | module.intro() |> show_intro() 10 | end 11 | end 12 | 13 | def show_intro(message) do 14 | (message <> "\n") 15 | |> Paint.green() 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/koans/keyword_lists_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule KeywordListsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "KeywordLists" do 6 | answers = [ 7 | "bar", 8 | "bar", 9 | {:multiple, ["bar", "baz"]}, 10 | {:multiple, [:foo, "bar"]}, 11 | "foo" 12 | ] 13 | 14 | test_all(KeywordLists, answers) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/koans/protocols_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ProtocolsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Protocols" do 6 | answers = [ 7 | {:multiple, ["Andre played violin", "Darcy performed ballet"]}, 8 | "Artist showed performance", 9 | Protocol.UndefinedError 10 | ] 11 | 12 | test_all(Protocols, answers) 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | "Thank you!" to all the contributors (alphabetical order): 2 | 3 | * Alex 4 | * Fabien Townsend 5 | * Felipe Seré 6 | * gemcfadyen 7 | * Jay Hayes 8 | * kamidev 9 | * Mahmut Surekci 10 | * Makis Otman 11 | * Nathan Walker 12 | * Rabea Gleissner 13 | * Ria Cataquian 14 | * Simone D'Amico 15 | * snikolau 16 | * Stephen Rufle 17 | * Trung Lê 18 | * Uku Taht 19 | * Zander Mackie 20 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | before_install: 2 | - sudo apt-get -qq update 3 | - sudo apt-get install -y inotify-tools 4 | language: elixir 5 | matrix: 6 | include: 7 | - elixir: 1.3.4 8 | otp_release: 19.1 9 | - elixir: 1.4.1 10 | otp_release: 19.1 11 | - elixir: 1.5.1 12 | otp_release: 20.0 13 | - elixir: 1.6.5 14 | otp_release: 20.0 15 | - elixir: 1.7.3 16 | otp_release: 21.0 17 | -------------------------------------------------------------------------------- /test/koans/map_sets_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MapSetsTest do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "MapSets" do 6 | answers = [ 7 | 3, 8 | {:multiple, [false, true]}, 9 | true, 10 | {:multiple, [true, false]}, 11 | true, 12 | false, 13 | false, 14 | true, 15 | 7, 16 | [1, 2, 3, 4, 5] 17 | ] 18 | 19 | test_all(MapSets, answers) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/koans/tuples_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TupleTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Tuples" do 6 | answers = [ 7 | {:a, 1, "hi"}, 8 | 3, 9 | "hi", 10 | {:a, "bye"}, 11 | {:a, :new_thing, "hi"}, 12 | {"Huey", "Dewey", "Louie"}, 13 | {:this, :is, :awesome}, 14 | [:this, :can, :be, :a, :list] 15 | ] 16 | 17 | test_all(Tuples, answers) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/koans/sigils_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule SigilsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Sigils" do 6 | answers = [ 7 | "This is a string", 8 | ~S("Welcome to the jungle", they said.), 9 | true, 10 | "1 + 1 = 2", 11 | ~S(1 + 1 = #{1+1}), 12 | ["Hello", "world"], 13 | ["Hello", "123"], 14 | ["Hello", ~S(#{1+1})] 15 | ] 16 | 17 | test_all(Sigils, answers) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/koans/strings_koan_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StringTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Strings" do 6 | answers = [ 7 | "hello", 8 | "1 + 1 = 2", 9 | "hello ", 10 | "hello world", 11 | "An incredible day", 12 | "incredible", 13 | "banana", 14 | "banana", 15 | "StringStringString", 16 | "LISTEN" 17 | ] 18 | 19 | test_all(Strings, answers) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/koans/comprehensions_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ComprehensionsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Comprehensions" do 6 | answers = [ 7 | [1, 4, 9, 16], 8 | [1, 4, 9, 16], 9 | ["Hello World", "Apple Pie"], 10 | ["little dogs", "little cats", "big dogs", "big cats"], 11 | [4, 5, 6], 12 | %{"Pecan" => "Pecan Pie", "Pumpkin" => "Pumpkin Pie"} 13 | ] 14 | 15 | test_all(Comprehensions, answers) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/koans/maps_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule MapsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Maps" do 6 | answers = [ 7 | "Jon", 8 | {:ok, 27}, 9 | :error, 10 | {:ok, "Kayaking"}, 11 | {:ok, 37}, 12 | {:ok, 16}, 13 | false, 14 | %{:first_name => "Jon", :last_name => "Snow"}, 15 | {:ok, "Baratheon"}, 16 | %{:first_name => "Jon", :last_name => "Snow"} 17 | ] 18 | 19 | test_all(Maps, answers) 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /test/executor_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ExecuteTest do 2 | use ExUnit.Case 3 | 4 | test "passes a koan" do 5 | assert :passed == Execute.run_module(PassingKoan) 6 | end 7 | 8 | test "stops at the first failing koan" do 9 | {:failed, %{file: file, line: line}, SampleKoan, _name} = Execute.run_module(SampleKoan) 10 | assert file == ~c'test/support/sample_koan.ex' 11 | assert line == 9 12 | end 13 | 14 | test "can access intro" do 15 | assert SampleKoan.intro() == "There is something\n" 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /lib/elixir_koans.ex: -------------------------------------------------------------------------------- 1 | defmodule ElixirKoans do 2 | @moduledoc false 3 | use Application 4 | 5 | def start(_type, _args) do 6 | children = [ 7 | %{id: Display, start: {Display, :start_link, []}}, 8 | %{id: Tracker, start: {Tracker, :start_link, []}}, 9 | %{id: Runner, start: {Runner, :start_link, []}}, 10 | %{id: Watcher, start: {Watcher, :start_link, []}} 11 | ] 12 | 13 | opts = [strategy: :one_for_one, name: ElixirKoans.Supervisor] 14 | Supervisor.start_link(children, opts) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /test/koans/structs_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule StructsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Structs" do 6 | answers = [ 7 | %Structs.Person{}, 8 | nil, 9 | "Joe", 10 | 33, 11 | {:ok, 22}, 12 | %Structs.Airline{plane: %Structs.Plane{maker: :airbus}, name: "Southwest"}, 13 | %Structs.Airline{plane: %Structs.Plane{maker: :boeing, passengers: 202}, name: "Southwest"}, 14 | %{plane: %{maker: :cessna}, name: "Southwest"} 15 | ] 16 | 17 | test_all(Structs, answers) 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /test/koans/lists_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ListsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Lists" do 6 | answers = [ 7 | 1, 8 | 3, 9 | [1, 2, :a, "b"], 10 | [1, 2], 11 | [:a, :c], 12 | [:a, :b], 13 | ["life", "life", "life"], 14 | [1, 2, 3, 4, 5], 15 | [1, 4, 2, 3], 16 | [10, 2, 3], 17 | [1, 2, 3], 18 | [1, 2, 3, 4], 19 | [1, 2, 3, 4], 20 | {1, 2, 3}, 21 | ["value"], 22 | [], 23 | ["value"] 24 | ] 25 | 26 | test_all(Lists, answers) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/koans/04_atoms.ex: -------------------------------------------------------------------------------- 1 | defmodule Atoms do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Atoms" 6 | 7 | koan "Atoms are constants where their name is their own value" do 8 | adam = :human 9 | assert adam == ___ 10 | end 11 | 12 | koan "It is surprising to find out that booleans are atoms" do 13 | assert is_atom(true) == ___ 14 | assert is_boolean(false) == ___ 15 | assert true == ___ 16 | assert false == ___ 17 | end 18 | 19 | koan "Like booleans, the nil value is also an atom" do 20 | assert is_atom(nil) == ___ 21 | assert nil == ___ 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /test/koans/processes_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ProcessesTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Processes" do 6 | answers = [ 7 | true, 8 | :running, 9 | true, 10 | true, 11 | {:multiple, [false, true]}, 12 | "hola!", 13 | true, 14 | {:multiple, ["hola!", "como se llama?"]}, 15 | :how_are_you?, 16 | {:multiple, ["O", "HAI"]}, 17 | {:multiple, ["foo", "bar"]}, 18 | {:waited_too_long, "I am impatient"}, 19 | {:exited, :random_reason}, 20 | :normal, 21 | :normal 22 | ] 23 | 24 | test_all(Processes, answers) 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Koans.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ 6 | app: :elixir_koans, 7 | version: "0.0.1", 8 | elixir: ">= 1.3.0 and < 2.0.0", 9 | elixirc_paths: elixirc_path(Mix.env()), 10 | deps: deps() 11 | ] 12 | end 13 | 14 | def application do 15 | [mod: {ElixirKoans, []}, applications: [:file_system, :logger]] 16 | end 17 | 18 | defp deps do 19 | [ 20 | {:file_system, "~> 1.1"}, 21 | {:credo, "~> 1.7", only: [:dev, :test], runtime: false} 22 | ] 23 | end 24 | 25 | defp elixirc_path(:test), do: ["lib/", "test/support"] 26 | defp elixirc_path(_), do: ["lib/"] 27 | end 28 | -------------------------------------------------------------------------------- /lib/display/notification.ex: -------------------------------------------------------------------------------- 1 | defmodule Display.Notifications do 2 | @moduledoc false 3 | alias Display.Paint 4 | 5 | def congratulate do 6 | Paint.green("\nYou have learned much. You must find your own path now.") 7 | end 8 | 9 | def invalid_koan(koan, modules) do 10 | koans_names = module_names(modules) 11 | "Did not find koan #{name(koan)} in " <> koans_names 12 | end 13 | 14 | defp module_names(modules) do 15 | modules 16 | |> Enum.map(&Atom.to_string/1) 17 | |> Enum.map_join(", ", &name/1) 18 | |> Paint.red() 19 | end 20 | 21 | defp name("Elixir." <> module), do: module 22 | defp name(module), do: name(Atom.to_string(module)) 23 | end 24 | -------------------------------------------------------------------------------- /test/koans/genservers_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule GenServersTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "GenServers" do 6 | answers = [ 7 | true, 8 | "3kr3t!", 9 | {:multiple, ["Apple Inc.", "MacBook Pro"]}, 10 | {:multiple, [["2.9 GHz Intel Core i5"], 8192, :intel_iris_graphics]}, 11 | "73x7!n9", 12 | {:error, "Incorrect password!"}, 13 | "Congrats! Your process was successfully named.", 14 | {:ok, "Laptop unlocked!"}, 15 | {:multiple, ["Laptop unlocked!", "Incorrect password!", "Jack Sparrow"]}, 16 | 1, 17 | {:multiple, ["the state", "the state"]} 18 | ] 19 | 20 | test_all(GenServers, answers) 21 | end 22 | end 23 | -------------------------------------------------------------------------------- /test/display/progress_bar_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ProgressBarTest do 2 | use ExUnit.Case 3 | 4 | alias Display.ProgressBar 5 | 6 | test "empty bar" do 7 | bar = ProgressBar.progress_bar(%{total: 12, current: 0}) 8 | assert bar == "| | 0 of 12 -> 0.0% complete" 9 | end 10 | 11 | test "puts counter on the right until half the koans are complete" do 12 | bar = ProgressBar.progress_bar(%{total: 12, current: 3}) 13 | assert bar == "|=======> | 3 of 12 -> 25.0% complete" 14 | end 15 | 16 | test "full bar" do 17 | bar = ProgressBar.progress_bar(%{total: 12, current: 12}) 18 | assert bar == "|=============================>| 12 of 12 -> 100.0% complete" 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /test/koans/pipe_operator_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PipeOperatorTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Pipe Operator" do 6 | answers = [ 7 | "HELLO-WORLD", 8 | "hello_world", 9 | [6, 8, 10], 10 | "hello, world", 11 | 20, 12 | "1-2-3", 13 | ["Alice", "Charlie"], 14 | ["QUICK", "BROWN", "JUMPS"], 15 | [a: 2, b: 4, c: 6], 16 | {:multiple, [{:ok, 84}, {:error, :invalid_number}]}, 17 | {:multiple, [["HELLO", "WORLD"], ["hello", "world"]]}, 18 | 250, 19 | 12, 20 | {:multiple, [2, 2, 1]}, 21 | 5, 22 | {:multiple, ["Result: 5.0", "Error: division_by_zero"]} 23 | ] 24 | 25 | test_all(PipeOperator, answers) 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/koans/numbers_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule NumbersTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Numbers" do 6 | answers = [ 7 | true, 8 | false, 9 | 1.0, 10 | 2, 11 | 1, 12 | 4, 13 | 4.0, 14 | false, 15 | 5, 16 | true, 17 | true, 18 | [5, 8, 1, 2, 7], 19 | 1234, 20 | "1234", 21 | 42, 22 | " years", 23 | {:multiple, [1, ".2"]}, 24 | 34.5, 25 | 1.5, 26 | 35.0, 27 | 34.3, 28 | 99.0, 29 | 12.34, 30 | {:multiple, [6.0, 5.0, 8.9, -5.567]}, 31 | {:multiple, [1, 10]}, 32 | {:multiple, [true, true, false]}, 33 | {:multiple, [true, false]} 34 | ] 35 | 36 | test_all(Numbers, answers) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/koans/enum_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule EnumTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Enums" do 6 | answers = [ 7 | 3, 8 | 3, 9 | 1, 10 | {:multiple, [2, ArgumentError]}, 11 | {:multiple, [true, false]}, 12 | {:multiple, [true, false]}, 13 | {:multiple, [true, false]}, 14 | [10, 20, 30], 15 | [1, 3], 16 | [2], 17 | [1, 2, 3], 18 | [1, 2, 3, 4, 5], 19 | [1, 2, 3], 20 | [a: 1, b: 2, c: 3], 21 | 2, 22 | nil, 23 | :no_such_element, 24 | 6, 25 | {:multiple, [[[1, 2], [3, 4], [5, 6]], [[1, 2, 3], [4, 5]]]}, 26 | [1, 10, 2, 20, 3, 30], 27 | {:multiple, [["apple", "apricot"], ["banana", "blueberry"]]}, 28 | [4, 8, 12] 29 | ] 30 | 31 | test_all(Enums, answers) 32 | end 33 | end 34 | -------------------------------------------------------------------------------- /lib/display/progress_bar.ex: -------------------------------------------------------------------------------- 1 | defmodule Display.ProgressBar do 2 | @moduledoc false 3 | @progress_bar_length 30 4 | 5 | def progress_bar(%{current: current, total: total}) do 6 | arrow = calculate_progress(current, total) |> build_arrow 7 | progress_percentage = calculate_percentage(current, total) 8 | 9 | "|" <> 10 | String.pad_trailing(arrow, @progress_bar_length) <> 11 | "| #{current} of #{total} -> #{progress_percentage}% complete" 12 | end 13 | 14 | defp calculate_progress(current, total) do 15 | round(current / total * @progress_bar_length) 16 | end 17 | 18 | defp calculate_percentage(current, total) do 19 | Float.round(current / total * 100, 1) 20 | end 21 | 22 | defp build_arrow(0), do: "" 23 | 24 | defp build_arrow(length) do 25 | String.duplicate("=", length - 1) <> ">" 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /test/koans/control_flow_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ControlFlowTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Control Flow" do 6 | answers = [ 7 | "yes", 8 | "math works", 9 | "will execute", 10 | {:multiple, ["falsy", "falsy", "truthy", "truthy", "truthy"]}, 11 | "matched with x = 2", 12 | {:multiple, ["positive", "zero", "negative"]}, 13 | {:multiple, ["empty", "one element", "two elements", "many elements"]}, 14 | "warm", 15 | {:multiple, [{:ok, 5}, {:error, "division by zero"}]}, 16 | {:multiple, ["Success: Hello", "Client error: 404", "Request failed: timeout"]}, 17 | {:multiple, 18 | ["positive even integer", "positive odd integer", "negative integer", "float", "other"]}, 19 | "verified active user" 20 | ] 21 | 22 | test_all(ControlFlow, answers) 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /test/koans/functions_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FunctionsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Functions" do 6 | answers = [ 7 | "Hello, World!", 8 | 3, 9 | {:multiple, ["One and Two", "Only One"]}, 10 | {:multiple, ["Hello Hello Hello ", "Hello Hello "]}, 11 | {:multiple, [:entire_list, :single_thing]}, 12 | {:multiple, ["10 is bigger than 5", "4 is not bigger than 27"]}, 13 | {:multiple, ["The number was zero", "The number was 5"]}, 14 | 6, 15 | 6, 16 | "Hi, Foo!", 17 | ["foo", "foo", "foo"], 18 | {:multiple, ["Success is no accident", "You just lost the game"]}, 19 | 100, 20 | 1000, 21 | "Full Name", 22 | {:multiple, [24, "hello_world"]}, 23 | {:multiple, ["GOOD", "good"]}, 24 | {:multiple, [12, 5]} 25 | ] 26 | 27 | test_all(Functions, answers) 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /test/koans/patterns_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule PatternsTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Pattern Matching" do 6 | answers = [ 7 | 1, 8 | {:multiple, [1, [2, 3, 4]]}, 9 | [1, 2, 3, 4], 10 | 3, 11 | "eggs, milk", 12 | "Honda", 13 | MatchError, 14 | {:multiple, [:make, "Honda"]}, 15 | [1, 2, 3], 16 | {:multiple, ["Meow", "Woof", "Eh?"]}, 17 | {:multiple, ["Mickey", "Donald", "I need a name!"]}, 18 | "barking", 19 | "Max", 20 | {:multiple, [true, false]}, 21 | "Max", 22 | 1, 23 | 2, 24 | {:multiple, ["The number One", "The number Two", "The number 3"]}, 25 | "same", 26 | 2, 27 | {:multiple, [30, "dark"]}, 28 | {:multiple, [1, 2, [3, 4, 5], 1]}, 29 | {:multiple, [5, :division_by_zero]} 30 | ] 31 | 32 | test_all(PatternMatching, answers) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | # ms 2 | timeout = 1000 3 | ExUnit.start(timeout: timeout) 4 | 5 | defmodule TestHarness do 6 | import ExUnit.Assertions 7 | 8 | def test_all(module, answers) do 9 | module.all_koans() 10 | |> check_answer_count(answers, module) 11 | |> Enum.zip(answers) 12 | |> run_all(module) 13 | |> check_results 14 | end 15 | 16 | defp check_answer_count(koans, answers, module) do 17 | koans_count = length(koans) 18 | answer_count = length(answers) 19 | 20 | if length(koans) > length(answers) do 21 | raise "Answer missing for #{module}. #{koans_count} koans, but only #{answer_count} answers." 22 | else 23 | koans 24 | end 25 | end 26 | 27 | defp check_results(results) do 28 | Enum.each(results, &assert(&1 == :passed)) 29 | end 30 | 31 | def run_all(pairs, module) do 32 | Enum.map(pairs, fn {koan, answer} -> Execute.run_koan(module, koan, [answer]) end) 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /lib/koans/07_keyword_lists.ex: -------------------------------------------------------------------------------- 1 | defmodule KeywordLists do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "KeywordLists" 6 | 7 | koan "Like maps, keyword lists are key-value pairs" do 8 | kw_list = [foo: "bar"] 9 | 10 | assert kw_list[:foo] == ___ 11 | end 12 | 13 | koan "Keys may be repeated, but only the first is accessed" do 14 | kw_list = [foo: "bar", foo: "baz"] 15 | 16 | assert kw_list[:foo] == ___ 17 | end 18 | 19 | koan "You could access the values of repeating key" do 20 | kw_list = [foo: "bar", foo1: "bar1", foo: "baz"] 21 | 22 | assert Keyword.get_values(kw_list, :foo) == [___, ___] 23 | end 24 | 25 | koan "Keyword lists are just special syntax for lists of two-element tuples" do 26 | assert [foo: "bar"] == [{___, ___}] 27 | end 28 | 29 | koan "But unlike maps, the keys in keyword lists must be atoms" do 30 | not_kw_list = [{"foo", "bar"}] 31 | 32 | assert_raise ArgumentError, fn -> not_kw_list[___] end 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /test/tracker_test.exs: -------------------------------------------------------------------------------- 1 | defmodule TrackerTest do 2 | use ExUnit.Case 3 | 4 | @sample_modules [SampleKoan, PassingKoan] 5 | 6 | test "can start" do 7 | Tracker.set_total(@sample_modules) 8 | assert Tracker.summarize() == %{total: 2, current: 0, visited_modules: []} 9 | end 10 | 11 | test "can be notified of completed koans" do 12 | Tracker.set_total(@sample_modules) 13 | Tracker.completed(SampleKoan, :"Hi there") 14 | assert Tracker.summarize() == %{total: 2, current: 1, visited_modules: [SampleKoan]} 15 | end 16 | 17 | test "multiple comletions of the same koan count only once" do 18 | Tracker.set_total(@sample_modules) 19 | Tracker.completed(SampleKoan, :"Hi there") 20 | Tracker.completed(SampleKoan, :"Hi there") 21 | assert Tracker.summarize() == %{total: 2, current: 1, visited_modules: [SampleKoan]} 22 | end 23 | 24 | test "knows when koans are not complete" do 25 | Tracker.set_total(@sample_modules) 26 | refute Tracker.complete?() 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/display/colours.ex: -------------------------------------------------------------------------------- 1 | defmodule Display.Paint do 2 | @moduledoc false 3 | def red(str), do: painter().red(str) 4 | def cyan(str), do: painter().cyan(str) 5 | def green(str), do: painter().green(str) 6 | def yellow(str), do: painter().yellow(str) 7 | 8 | defp painter do 9 | case Mix.env() do 10 | :test -> Display.Uncoloured 11 | _ -> Display.Colours 12 | end 13 | end 14 | end 15 | 16 | defmodule Display.Colours do 17 | @moduledoc false 18 | alias IO.ANSI 19 | 20 | def red(str), do: colourize(ANSI.red(), str) 21 | def cyan(str), do: colourize(ANSI.cyan(), str) 22 | def green(str), do: colourize(ANSI.green(), str) 23 | def yellow(str), do: colourize(ANSI.yellow(), str) 24 | 25 | defp colourize(color, message) do 26 | Enum.join([color, message, ANSI.reset()], "") 27 | end 28 | end 29 | 30 | defmodule Display.Uncoloured do 31 | @moduledoc false 32 | def red(str), do: str 33 | def cyan(str), do: str 34 | def green(str), do: str 35 | def yellow(str), do: str 36 | end 37 | -------------------------------------------------------------------------------- /lib/koans/01_equalities.ex: -------------------------------------------------------------------------------- 1 | defmodule Equalities do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro """ 6 | Welcome to the Elixir koans. 7 | Let these be your first humble steps towards learning a new language. 8 | 9 | The path laid in front of you is one of many. 10 | """ 11 | 12 | # Replace ___ with the answer to make the koan pass. 13 | koan "We shall contemplate truth by testing reality, via equality" do 14 | assert true == ___ 15 | end 16 | 17 | koan "Not something is the opposite of it" do 18 | assert !true == ___ 19 | end 20 | 21 | koan "To understand reality, we must compare our expectations against reality" do 22 | assert 2 == 1 + ___ 23 | end 24 | 25 | koan "Some things may appear different, but be the same" do 26 | assert 1 == 2 / ___ 27 | end 28 | 29 | koan "Unless they actually are different" do 30 | assert 3.2 != ___ 31 | end 32 | 33 | koan "Some may be looking for bigger things" do 34 | assert ___ > 3 35 | end 36 | 37 | koan "Others are happy with less" do 38 | assert ___ < 3 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, 3 | "credo": {:hex, :credo, "1.7.12", "9e3c20463de4b5f3f23721527fcaf16722ec815e70ff6c60b86412c695d426c1", [:mix], [{:bunt, "~> 0.2.1 or ~> 1.0", [hex: :bunt, repo: "hexpm", optional: false]}, {:file_system, "~> 0.2 or ~> 1.0", [hex: :file_system, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "8493d45c656c5427d9c729235b99d498bd133421f3e0a683e5c1b561471291e5"}, 4 | "file_system": {:hex, :file_system, "1.1.0", "08d232062284546c6c34426997dd7ef6ec9f8bbd090eb91780283c9016840e8f", [:mix], [], "hexpm", "bfcf81244f416871f2a2e15c1b515287faa5db9c6bcf290222206d120b3d43f6"}, 5 | "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, 6 | } 7 | -------------------------------------------------------------------------------- /test/koans/with_statement_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule WithStatementTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "With Statement" do 6 | answers = [ 7 | {:multiple, [{:ok, 9}, {:error, :invalid_number}]}, 8 | {:multiple, [{:ok, "Adult user: Alice"}, {:error, :underage}, {:error, :missing_data}]}, 9 | {:multiple, 10 | [ 11 | {:ok, 2}, 12 | {:error, "Cannot divide by zero"}, 13 | {:error, "Cannot take square root of negative number"} 14 | ]}, 15 | {:multiple, [{:ok, "user@example.com"}, {:error, :invalid_email}, {:error, :missing_data}]}, 16 | {:multiple, 17 | [{:ok, 50}, {:error, :not_positive}, {:error, :result_too_large}, {:error, :not_a_number}]}, 18 | {:ok, %{id: 1}}, 19 | {:multiple, [8, "5"]}, 20 | {:multiple, 21 | [ 22 | {:ok, "step3_step2_step1_valid"}, 23 | {:error, "Failed at step 1: invalid input"}, 24 | {:error, "Failed at step 2: processing error"} 25 | ]} 26 | ] 27 | 28 | test_all(WithStatement, answers) 29 | end 30 | end 31 | -------------------------------------------------------------------------------- /lib/koans/11_sigils.ex: -------------------------------------------------------------------------------- 1 | defmodule Sigils do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Sigils" 6 | 7 | koan "The ~s sigil is a different way of expressing string literals" do 8 | assert ~s{This is a string} == ___ 9 | end 10 | 11 | koan "Sigils are useful to avoid escaping quotes in strings" do 12 | assert "\"Welcome to the jungle\", they said." == ___ 13 | end 14 | 15 | koan "Sigils can use different delimiters" do 16 | matches? = ~s{This works!} == ~s[This works!] 17 | assert matches? == ___ 18 | end 19 | 20 | koan "The lowercase ~s sigil supports string interpolation" do 21 | assert ~s[1 + 1 = #{1 + 1}] == ___ 22 | end 23 | 24 | koan "The ~S sigil is similar to ~s but doesn't do interpolation" do 25 | assert ~S[1 + 1 = #{1+1}] == ___ 26 | end 27 | 28 | koan "The ~w sigil creates word lists" do 29 | assert ~w(Hello world) == ___ 30 | end 31 | 32 | koan "The ~w sigil also allows interpolation" do 33 | assert ~w(Hello 1#{1 + 1}3) == ___ 34 | end 35 | 36 | koan "The ~W sigil behaves to ~w as ~S behaves to ~s" do 37 | assert ~W(Hello #{1+1}) == ___ 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2015 Elixir Koans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/koans/05_tuples.ex: -------------------------------------------------------------------------------- 1 | defmodule Tuples do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Tuples" 6 | 7 | koan "Tuples can contain different things" do 8 | assert {:a, 1, "hi"} == ___ 9 | end 10 | 11 | koan "Tuples have a size" do 12 | assert tuple_size({:a, :b, :c}) == ___ 13 | end 14 | 15 | koan "You can pull out individual elements" do 16 | assert elem({:a, "hi"}, 1) == ___ 17 | end 18 | 19 | koan "You can change individual elements of a tuple" do 20 | assert put_elem({:a, "hi"}, 1, "bye") == ___ 21 | end 22 | 23 | koan "You can also simply extend a tuple with new stuff" do 24 | assert Tuple.insert_at({:a, "hi"}, 1, :new_thing) == ___ 25 | end 26 | 27 | koan "Add things at the end (by constructing a new tuple)" do 28 | {first, second} = {"Huey", "Dewey"} 29 | extended = {first, second, "Louie"} 30 | assert extended == ___ 31 | end 32 | 33 | koan "Or remove them" do 34 | assert Tuple.delete_at({:this, :is, :not, :awesome}, 2) == ___ 35 | end 36 | 37 | koan "Turn it into a list in case you need it" do 38 | assert Tuple.to_list({:this, :can, :be, :a, :list}) == ___ 39 | end 40 | end 41 | -------------------------------------------------------------------------------- /test/koans/error_handling_koans_test.exs: -------------------------------------------------------------------------------- 1 | defmodule ErrorHandlingTests do 2 | use ExUnit.Case 3 | import TestHarness 4 | 5 | test "Error Handling" do 6 | answers = [ 7 | {:multiple, [{:ok, 123}, {:error, :invalid_format}]}, 8 | "Result: 5.0", 9 | "Cannot divide by zero!", 10 | {:multiple, [{:ok, 2}, {:error, :invalid_argument}, {:error, "abc is not a list"}]}, 11 | {:multiple, 12 | [ 13 | {:error, :arithmetic}, 14 | {:error, :missing_key}, 15 | {:error, :invalid_argument}, 16 | {:ok, "success"} 17 | ]}, 18 | "caught thrown value", 19 | :returned_value, 20 | {:multiple, [:success, "it worked"]}, 21 | "caught custom error: custom failure", 22 | "key not found", 23 | "caught normal exit", 24 | {:multiple, 25 | [ 26 | {:error, {:exception, "connection failed"}}, 27 | {:error, :timeout}, 28 | {:error, :invalid_query}, 29 | {:ok, "data retrieved"} 30 | ]}, 31 | {:multiple, [:conversion_error, "user input processing"]} 32 | ] 33 | 34 | test_all(ErrorHandling, answers) 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/meditate.ex: -------------------------------------------------------------------------------- 1 | defmodule Mix.Tasks.Meditate do 2 | @moduledoc false 3 | use Mix.Task 4 | 5 | @shortdoc "Start the koans" 6 | 7 | def run(args) do 8 | Application.ensure_all_started(:elixir_koans) 9 | Code.compiler_options(ignore_module_conflict: true) 10 | 11 | {parsed, _, _} = OptionParser.parse(args, switches: []) 12 | 13 | modules = 14 | parsed 15 | |> initial_module 16 | |> ok? 17 | |> Runner.modules_to_run() 18 | 19 | Tracker.set_total(modules) 20 | Tracker.notify_on_complete(self()) 21 | 22 | set_clear_screen(parsed) 23 | Runner.run(modules) 24 | 25 | Tracker.wait_until_complete() 26 | Display.congratulate() 27 | end 28 | 29 | defp initial_module(parsed) do 30 | name = Keyword.get(parsed, :koan, "Equalities") 31 | String.to_atom("Elixir." <> name) 32 | end 33 | 34 | defp set_clear_screen(parsed) do 35 | if Keyword.has_key?(parsed, :no_clear_screen) do 36 | Display.disable_clear() 37 | end 38 | end 39 | 40 | defp ok?(koan) do 41 | if Runner.koan?(koan) do 42 | koan 43 | else 44 | Display.invalid_koan(koan, Runner.modules()) 45 | exit(:normal) 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /lib/koans/20_comprehensions.ex: -------------------------------------------------------------------------------- 1 | defmodule Comprehensions do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "A comprehension is made of three parts: generators, filters, and collectibles. We will look at how these interact with each other" 6 | 7 | koan "The generator, `n <- [1, 2, 3, 4]`, is providing the values for our comprehension" do 8 | assert for(n <- [1, 2, 3, 4], do: n * n) == ___ 9 | end 10 | 11 | koan "Any enumerable can be a generator" do 12 | assert for(n <- 1..4, do: n * n) == ___ 13 | end 14 | 15 | koan "A generator specifies how to extract values from a collection" do 16 | collection = [["Hello", "World"], ["Apple", "Pie"]] 17 | assert for([a, b] <- collection, do: "#{a} #{b}") == ___ 18 | end 19 | 20 | koan "You can use multiple generators at once" do 21 | assert for(x <- ["little", "big"], y <- ["dogs", "cats"], do: "#{x} #{y}") == ___ 22 | end 23 | 24 | koan "Use a filter to reduce your work" do 25 | assert for(n <- [1, 2, 3, 4, 5, 6], n > 3, do: n) == ___ 26 | end 27 | 28 | koan "Add the result of a comprehension to an existing collection" do 29 | collection = for x <- ["Pecan", "Pumpkin"], into: %{}, do: {x, "#{x} Pie"} 30 | assert collection == ___ 31 | end 32 | end 33 | -------------------------------------------------------------------------------- /lib/koans/02_strings.ex: -------------------------------------------------------------------------------- 1 | defmodule Strings do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Strings" 6 | 7 | koan "Strings are there to represent text" do 8 | assert "hello" == ___ 9 | end 10 | 11 | koan "Values may be inserted into strings by interpolation" do 12 | assert "1 + 1 = #{1 + 1}" == ___ 13 | end 14 | 15 | koan "They can be put together" do 16 | assert "hello world" == ___ <> "world" 17 | end 18 | 19 | koan "Or pulled apart into a list when needed" do 20 | assert ["hello", "world"] == String.split(___, " ") 21 | end 22 | 23 | koan "Be careful, a message may be altered" do 24 | assert String.replace("An awful day", "awful", "incredible") == ___ 25 | end 26 | 27 | koan "But strings never lie about themselves" do 28 | assert true == String.contains?("An incredible day", ___) 29 | end 30 | 31 | koan "Sometimes you want just the opposite of what is given" do 32 | assert ___ == String.reverse("ananab") 33 | end 34 | 35 | koan "Other times a little cleaning is in order" do 36 | assert String.trim(" \n banana\n ") == ___ 37 | end 38 | 39 | koan "Repetition is the mother of learning" do 40 | assert String.duplicate("String", 3) == ___ 41 | end 42 | 43 | koan "Strings can be louder when necessary" do 44 | assert String.upcase("listen") == ___ 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/watcher.ex: -------------------------------------------------------------------------------- 1 | defmodule Watcher do 2 | @moduledoc false 3 | use GenServer 4 | 5 | def start_link do 6 | GenServer.start_link(__MODULE__, dirs: ["lib/koans"]) 7 | end 8 | 9 | def init(args) do 10 | {:ok, watcher_pid} = FileSystem.start_link(args) 11 | FileSystem.subscribe(watcher_pid) 12 | {:ok, %{watcher_pid: watcher_pid}} 13 | end 14 | 15 | def handle_info({:file_event, watcher_pid, {path, events}}, %{watcher_pid: watcher_pid} = state) do 16 | # respond to renamed as well due to that some editors use temporary files for atomic writes (ex: TextMate) 17 | if Enum.member?(events, :modified) || Enum.member?(events, :renamed) do 18 | path |> normalize |> reload 19 | end 20 | 21 | {:noreply, state} 22 | end 23 | 24 | defp reload(file) do 25 | if String.match?(file, Runner.koan_path_pattern()) do 26 | try do 27 | file 28 | |> portable_load_file 29 | |> Enum.map(&elem(&1, 0)) 30 | |> Enum.find(&Runner.koan?/1) 31 | |> Runner.modules_to_run() 32 | |> Runner.run() 33 | rescue 34 | e -> Display.show_compile_error(e) 35 | end 36 | end 37 | end 38 | 39 | defp portable_load_file(file) do 40 | Code.compile_file(file) 41 | end 42 | 43 | defp normalize(file) do 44 | String.replace_suffix(file, "___jb_tmp___", "") 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /lib/koans/17_agents.ex: -------------------------------------------------------------------------------- 1 | defmodule Agents do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Agents" 6 | 7 | koan "Agents maintain state, so you can ask them about it" do 8 | {:ok, pid} = Agent.start_link(fn -> "Hi there" end) 9 | assert Agent.get(pid, & &1) == ___ 10 | end 11 | 12 | koan "Agents may also be named so that you don't have to keep the pid around" do 13 | Agent.start_link(fn -> "Why hello" end, name: AgentSmith) 14 | assert Agent.get(AgentSmith, & &1) == ___ 15 | end 16 | 17 | koan "Update to update the state" do 18 | Agent.start_link(fn -> "Hi there" end, name: :greeter) 19 | 20 | Agent.update(:greeter, fn old -> 21 | String.upcase(old) 22 | end) 23 | 24 | assert Agent.get(:greeter, & &1) == ___ 25 | end 26 | 27 | koan "Use get_and_update when you need to read and change a value in one go" do 28 | Agent.start_link(fn -> ["Milk"] end, name: :groceries) 29 | 30 | old_list = 31 | Agent.get_and_update(:groceries, fn old -> 32 | {old, ["Bread" | old]} 33 | end) 34 | 35 | assert old_list == ___ 36 | assert Agent.get(:groceries, & &1) == ___ 37 | end 38 | 39 | koan "Somebody has to switch off the light at the end of the day" do 40 | {:ok, pid} = Agent.start_link(fn -> "Fin." end, name: :stoppable) 41 | 42 | Agent.stop(:stoppable) 43 | 44 | assert Process.alive?(pid) == ___ 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /.github/workflows/elixir.yml: -------------------------------------------------------------------------------- 1 | name: Elixir CI 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: [ master ] 7 | pull_request: 8 | branches: [ master ] 9 | 10 | jobs: 11 | build: 12 | runs-on: ubuntu-latest 13 | 14 | env: 15 | MIX_ENV: test 16 | 17 | strategy: 18 | matrix: 19 | elixir: [1.18.4] 20 | otp: [28.0.2] 21 | 22 | steps: 23 | - uses: actions/checkout@v4 24 | - name: System Dependencies 25 | run: | 26 | sudo apt update 27 | sudo apt install -y inotify-tools 28 | - name: Setup Elixir 29 | uses: erlef/setup-beam@v1 30 | with: 31 | otp-version: ${{ matrix.otp }} 32 | elixir-version: ${{ matrix.elixir }} 33 | - name: Restore dependencies cache 34 | uses: actions/cache@v4 35 | with: 36 | path: | 37 | deps 38 | _build 39 | key: mix-${{ runner.os }}-${{matrix.elixir}}-${{matrix.otp}}-${{ hashFiles('**/mix.lock') }} 40 | restore-keys: | 41 | mix-${{ runner.os }}-${{matrix.elixir}}-${{matrix.otp}}- 42 | 43 | - name: Install dependencies 44 | run: mix deps.get 45 | - name: Check code format 46 | run: mix format --check-formatted 47 | - name: Run static analysis 48 | run: mix credo --strict 49 | - name: Check for unneeded dependencies 50 | run: mix deps.unlock --check-unused 51 | - name: Run tests 52 | run: mix test 53 | -------------------------------------------------------------------------------- /lib/blanks.ex: -------------------------------------------------------------------------------- 1 | defmodule Blanks do 2 | @moduledoc false 3 | def replace(ast, replacements) do 4 | replacements = List.wrap(replacements) 5 | 6 | ast 7 | |> Macro.prewalk(replacements, &pre/2) 8 | |> elem(0) 9 | end 10 | 11 | defp pre({:assert_receive, _, args} = node, replacements) do 12 | {args, replacements} = Macro.prewalk(args, replacements, &pre_pin/2) 13 | {put_elem(node, 2, args), replacements} 14 | end 15 | 16 | defp pre({:___, _, _}, [first | remainder]), do: {first, remainder} 17 | defp pre(node, acc), do: {node, acc} 18 | 19 | defp pre_pin({:___, _, _}, [first | remainder]), do: {pin(first), remainder} 20 | defp pre_pin(node, acc), do: {node, acc} 21 | 22 | defp pin(var) when is_tuple(var) do 23 | quote do 24 | ^unquote(var) 25 | end 26 | end 27 | 28 | defp pin(var), do: var 29 | 30 | def count(ast) do 31 | ast 32 | |> Macro.prewalk(0, &count/2) 33 | |> elem(1) 34 | end 35 | 36 | defp count({:___, _, _} = node, acc), do: {node, acc + 1} 37 | defp count(node, acc), do: {node, acc} 38 | 39 | def replace_line({:__block__, meta, lines}, replacement_fn) do 40 | replaced_lines = 41 | Enum.map(lines, fn line -> 42 | replace_line(line, replacement_fn) 43 | end) 44 | 45 | {:__block__, meta, replaced_lines} 46 | end 47 | 48 | def replace_line(line, replacement_fn) do 49 | if count(line) > 0 do 50 | replacement_fn.(line) 51 | else 52 | line 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /lib/koans/16_tasks.ex: -------------------------------------------------------------------------------- 1 | defmodule Tasks do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Tasks" 6 | 7 | koan "Tasks can be used for asynchronous computations with results" do 8 | task = Task.async(fn -> 3 * 3 end) 9 | do_other_stuff() 10 | assert Task.await(task) + 1 == ___ 11 | end 12 | 13 | koan "If you don't need a result, use start_link/1" do 14 | {result, _pid} = Task.start_link(fn -> 1 + 1 end) 15 | assert result == ___ 16 | end 17 | 18 | koan "Yield returns nil if the task isn't done yet" do 19 | handle = 20 | Task.async(fn -> 21 | :timer.sleep(100) 22 | 3 * 3 23 | end) 24 | 25 | assert Task.yield(handle, 10) == ___ 26 | end 27 | 28 | koan "Tasks can be aborted with shutdown" do 29 | handle = 30 | Task.async(fn -> 31 | :timer.sleep(100) 32 | 3 * 3 33 | end) 34 | 35 | %Task{pid: pid} = handle 36 | Task.shutdown(handle) 37 | 38 | assert Process.alive?(pid) == ___ 39 | end 40 | 41 | koan "Shutdown will give you an answer if it has it" do 42 | handle = Task.async(fn -> 3 * 3 end) 43 | :timer.sleep(10) 44 | assert Task.shutdown(handle) == {:ok, ___} 45 | end 46 | 47 | koan "You can yield to multiple tasks at once and extract the results" do 48 | squares = 49 | [1, 2, 3, 4] 50 | |> Enum.map(fn number -> Task.async(fn -> number * number end) end) 51 | |> Task.yield_many(100) 52 | |> Enum.map(fn {_task, {:ok, result}} -> result end) 53 | 54 | assert squares == ___ 55 | end 56 | 57 | def do_other_stuff do 58 | :timer.sleep(50) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/execute.ex: -------------------------------------------------------------------------------- 1 | defmodule Execute do 2 | @moduledoc false 3 | def run_module(module, callback \\ fn _result, _module, _koan -> nil end) do 4 | module.all_koans() 5 | |> Enum.reduce_while(:passed, fn koan, _ -> 6 | module 7 | |> run_koan(koan) 8 | |> hook(module, koan, callback) 9 | |> continue? 10 | end) 11 | end 12 | 13 | defp hook(result, module, koan, callback) do 14 | callback.(result, module, koan) 15 | result 16 | end 17 | 18 | defp continue?(:passed), do: {:cont, :passed} 19 | defp continue?(result), do: {:halt, result} 20 | 21 | def run_koan(module, name, args \\ []) do 22 | parent = self() 23 | spawn(fn -> exec(module, name, args, parent) end) 24 | listen_for_result(module, name) 25 | end 26 | 27 | def listen_for_result(module, name) do 28 | receive do 29 | :ok -> :passed 30 | %{error: _} = failure -> {:failed, failure, module, name} 31 | _ -> listen_for_result(module, name) 32 | end 33 | end 34 | 35 | defp exec(module, name, args, parent) do 36 | result = apply(module, name, args) 37 | send(parent, expand(result, module)) 38 | Process.exit(self(), :kill) 39 | end 40 | 41 | defp expand(:ok, _), do: :ok 42 | 43 | defp expand({:error, stacktrace, exception}, module) do 44 | {file, line} = 45 | stacktrace 46 | |> Enum.drop_while(&(!in_koan?(&1, module))) 47 | |> List.first() 48 | |> extract_file_and_line 49 | 50 | %{error: exception, file: file, line: line} 51 | end 52 | 53 | defp in_koan?({module, _, _, _}, koan), do: module == koan 54 | 55 | defp extract_file_and_line({_, _, _, [file: file, line: line]}) do 56 | {file, line} 57 | end 58 | end 59 | -------------------------------------------------------------------------------- /lib/koans/19_protocols.ex: -------------------------------------------------------------------------------- 1 | defmodule Protocols do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Want to follow the rules? Adhere to the protocol!" 6 | 7 | defprotocol(Artist, do: def(perform(artist))) 8 | 9 | defimpl Artist, for: Any do 10 | def perform(_) do 11 | "Artist showed performance" 12 | end 13 | end 14 | 15 | defmodule Painter do 16 | @moduledoc false 17 | @derive Artist 18 | defstruct name: "" 19 | end 20 | 21 | defmodule Musician do 22 | @moduledoc false 23 | defstruct(name: "", instrument: "") 24 | end 25 | 26 | defmodule Dancer do 27 | @moduledoc false 28 | defstruct(name: "", dance_style: "") 29 | end 30 | 31 | defmodule Physicist do 32 | @moduledoc false 33 | defstruct(name: "") 34 | end 35 | 36 | defimpl Artist, for: Musician do 37 | def perform(musician) do 38 | "#{musician.name} played #{musician.instrument}" 39 | end 40 | end 41 | 42 | defimpl Artist, for: Dancer do 43 | def perform(dancer), do: "#{dancer.name} performed #{dancer.dance_style}" 44 | end 45 | 46 | koan "Sharing an interface is the secret at school" do 47 | musician = %Musician{name: "Andre", instrument: "violin"} 48 | dancer = %Dancer{name: "Darcy", dance_style: "ballet"} 49 | 50 | assert Artist.perform(musician) == ___ 51 | assert Artist.perform(dancer) == ___ 52 | end 53 | 54 | koan "Sometimes we all use the same" do 55 | painter = %Painter{name: "Emily"} 56 | assert Artist.perform(painter) == ___ 57 | end 58 | 59 | koan "If you are not an artist, you can't show performance" do 60 | assert_raise ___, fn -> 61 | Artist.perform(%Physicist{name: "Delia"}) 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /lib/display/failure.ex: -------------------------------------------------------------------------------- 1 | defmodule Display.Failure do 2 | @moduledoc false 3 | alias Display.Paint 4 | 5 | @no_value :ex_unit_no_meaningful_value 6 | 7 | def format_failure(%{ 8 | error: %ExUnit.AssertionError{expr: @no_value, message: message}, 9 | file: file, 10 | line: line 11 | }) do 12 | """ 13 | #{Paint.cyan("Assertion failed in #{file}:#{line}")} 14 | #{Paint.red(message)} 15 | """ 16 | end 17 | 18 | def format_failure(%{error: %ExUnit.AssertionError{expr: expr} = error, file: file, line: line}) do 19 | """ 20 | #{Paint.cyan("Assertion failed in #{file}:#{line}")} 21 | #{Paint.red(Macro.to_string(expr))} 22 | """ 23 | |> format_inequality(error) 24 | end 25 | 26 | def format_failure(%{error: error, file: file, line: line}) do 27 | """ 28 | #{Paint.cyan("Error in #{file}:#{line}")} 29 | #{format_error(error)} 30 | """ 31 | end 32 | 33 | defp format_inequality(message, %{left: @no_value, right: @no_value}) do 34 | message 35 | end 36 | 37 | defp format_inequality(message, %{left: @no_value, right: match_value}) do 38 | """ 39 | #{message} 40 | value does not match: #{match_value |> inspect |> Paint.yellow()} 41 | """ 42 | end 43 | 44 | defp format_inequality(message, %{left: left, right: right}) do 45 | """ 46 | #{message} 47 | left: #{left |> inspect |> Paint.yellow()} 48 | right: #{right |> inspect |> Paint.yellow()} 49 | """ 50 | end 51 | 52 | defp format_error(error) do 53 | trace = Process.info(self(), :current_stacktrace) |> Enum.take(2) 54 | Paint.red(Exception.format(:error, error, trace)) 55 | end 56 | 57 | def show_compile_error(error) do 58 | format_error(error) 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /lib/koans/08_maps.ex: -------------------------------------------------------------------------------- 1 | defmodule Maps do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Maps" 6 | 7 | @person %{ 8 | first_name: "Jon", 9 | last_name: "Snow", 10 | age: 27 11 | } 12 | 13 | koan "Maps represent structured data, like a person" do 14 | assert @person == %{first_name: ___, last_name: "Snow", age: 27} 15 | end 16 | 17 | koan "Fetching a value returns a tuple with ok when it exists" do 18 | assert Map.fetch(@person, :age) == ___ 19 | end 20 | 21 | koan "Or the atom :error when it doesn't" do 22 | assert Map.fetch(@person, :family) == ___ 23 | end 24 | 25 | koan "Extending a map is as simple as adding a new pair" do 26 | person_with_hobby = Map.put(@person, :hobby, "Kayaking") 27 | assert Map.fetch(person_with_hobby, :hobby) == ___ 28 | end 29 | 30 | koan "Put can also overwrite existing values" do 31 | older_person = Map.put(@person, :age, 37) 32 | assert Map.fetch(older_person, :age) == ___ 33 | end 34 | 35 | koan "Or you can use some syntactic sugar for existing elements" do 36 | younger_person = %{@person | age: 16} 37 | assert Map.fetch(younger_person, :age) == ___ 38 | end 39 | 40 | koan "Can remove pairs by key" do 41 | without_age = Map.delete(@person, :age) 42 | assert Map.has_key?(without_age, :age) == ___ 43 | end 44 | 45 | koan "Can merge maps" do 46 | assert Map.merge(%{first_name: "Jon"}, %{last_name: "Snow"}) == ___ 47 | end 48 | 49 | koan "When merging, the last map wins" do 50 | merged = Map.merge(@person, %{last_name: "Baratheon"}) 51 | assert Map.fetch(merged, :last_name) == ___ 52 | end 53 | 54 | koan "You can also select sub-maps out of a larger map" do 55 | assert Map.take(@person, [:first_name, :last_name]) == ___ 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/display/failure_test.exs: -------------------------------------------------------------------------------- 1 | defmodule FailureTests do 2 | use ExUnit.Case 3 | alias Display.Failure 4 | 5 | test "assertion failure with proper expression" do 6 | error = error(%ExUnit.AssertionError{expr: "hi"}) 7 | 8 | assert Failure.format_failure(error) == "Assertion failed in some_file.ex:42\n\"hi\"\n" 9 | end 10 | 11 | test "assertion failure with message" do 12 | error = error(%ExUnit.AssertionError{expr: :ex_unit_no_meaningful_value, message: "hola"}) 13 | 14 | assert Failure.format_failure(error) == "Assertion failed in some_file.ex:42\nhola\n" 15 | end 16 | 17 | test "equality failure" do 18 | error = error(%ExUnit.AssertionError{expr: quote(do: :lol == :wat), left: :lol, right: :wat}) 19 | 20 | assert Failure.format_failure(error) == """ 21 | Assertion failed in some_file.ex:42 22 | :lol == :wat 23 | 24 | left: :lol 25 | right: :wat 26 | """ 27 | end 28 | 29 | test "match failure" do 30 | error = error(%ExUnit.AssertionError{expr: quote(do: match?(:lol, :wat)), right: :wat}) 31 | 32 | assert Failure.format_failure(error) == """ 33 | Assertion failed in some_file.ex:42 34 | match?(:lol, :wat) 35 | 36 | value does not match: :wat 37 | """ 38 | end 39 | 40 | test "only offending lines are displayed for errors" do 41 | [koan] = SingleArity.all_koans() 42 | error = apply(SingleArity, koan, []) |> Tuple.to_list() |> List.last() |> error 43 | 44 | assert Failure.format_failure(error) == """ 45 | Assertion failed in some_file.ex:42\nmatch?(:foo, ___) 46 | """ 47 | end 48 | 49 | defp error(error) do 50 | %{ 51 | error: error, 52 | file: "some_file.ex", 53 | line: 42 54 | } 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/koans/10_structs.ex: -------------------------------------------------------------------------------- 1 | defmodule Structs do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Structs" 6 | 7 | defmodule Person do 8 | @moduledoc false 9 | defstruct [:name, :age] 10 | end 11 | 12 | koan "Structs are defined and named after a module" do 13 | person = %Person{} 14 | assert person == ___ 15 | end 16 | 17 | koan "Unless previously defined, fields begin as nil" do 18 | nobody = %Person{} 19 | assert nobody.age == ___ 20 | end 21 | 22 | koan "You can pass initial values to structs" do 23 | joe = %Person{name: "Joe", age: 23} 24 | assert joe.name == ___ 25 | end 26 | 27 | koan "Update fields with the cons '|' operator" do 28 | joe = %Person{name: "Joe", age: 23} 29 | older = %{joe | age: joe.age + 10} 30 | assert older.age == ___ 31 | end 32 | 33 | koan "Struct can be treated like maps" do 34 | silvia = %Person{age: 22, name: "Silvia"} 35 | 36 | assert Map.fetch(silvia, :age) == ___ 37 | end 38 | 39 | defmodule Plane do 40 | @moduledoc false 41 | defstruct passengers: 0, maker: :boeing 42 | end 43 | 44 | defmodule Airline do 45 | @moduledoc false 46 | defstruct plane: %Plane{}, name: "Southwest" 47 | end 48 | 49 | koan "Use the put_in macro to replace a nested value" do 50 | airline = %Airline{} 51 | assert put_in(airline.plane.maker, :airbus) == ___ 52 | end 53 | 54 | koan "Use the update_in macro to modify a nested value" do 55 | airline = %Airline{plane: %Plane{passengers: 200}} 56 | assert update_in(airline.plane.passengers, fn x -> x + 2 end) == ___ 57 | end 58 | 59 | koan "Use the put_in macro with atoms to replace a nested value in a non-struct" do 60 | airline = %{plane: %{maker: :boeing}, name: "Southwest"} 61 | assert put_in(airline[:plane][:maker], :cessna) == ___ 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /lib/tracker.ex: -------------------------------------------------------------------------------- 1 | defmodule Tracker do 2 | @moduledoc false 3 | alias __MODULE__ 4 | 5 | defstruct total: 0, 6 | koans: MapSet.new(), 7 | visited_modules: MapSet.new(), 8 | on_complete: :noop 9 | 10 | def start_link do 11 | Agent.start_link(fn -> %Tracker{} end, name: __MODULE__) 12 | end 13 | 14 | def notify_on_complete(pid) do 15 | Agent.update(__MODULE__, fn state -> %{state | on_complete: pid} end) 16 | end 17 | 18 | def set_total(modules) do 19 | total = 20 | modules 21 | |> Enum.flat_map(& &1.all_koans()) 22 | |> Enum.count() 23 | 24 | Agent.update(__MODULE__, fn _ -> %Tracker{total: total} end) 25 | end 26 | 27 | def completed(module, koan) do 28 | Agent.update(__MODULE__, &mark_koan_completed(&1, module, koan)) 29 | 30 | if complete?() do 31 | Agent.cast(__MODULE__, fn state -> 32 | send(state.on_complete, {self(), :complete}) 33 | state 34 | end) 35 | end 36 | end 37 | 38 | def wait_until_complete do 39 | pid = Process.whereis(Tracker) 40 | 41 | receive do 42 | {^pid, :complete} -> :ok 43 | end 44 | end 45 | 46 | defp mark_koan_completed(state, module, koan) do 47 | %{ 48 | state 49 | | koans: MapSet.put(state.koans, koan), 50 | visited_modules: MapSet.put(state.visited_modules, module) 51 | } 52 | end 53 | 54 | def visited do 55 | summarize()[:visited_modules] 56 | end 57 | 58 | def complete? do 59 | %{total: total, current: completed} = summarize() 60 | total == completed 61 | end 62 | 63 | def summarize do 64 | state = Agent.get(__MODULE__, & &1) 65 | 66 | %{ 67 | total: state.total, 68 | current: MapSet.size(state.koans), 69 | visited_modules: MapSet.to_list(state.visited_modules) 70 | } 71 | end 72 | end 73 | -------------------------------------------------------------------------------- /lib/display.ex: -------------------------------------------------------------------------------- 1 | defmodule Display do 2 | @moduledoc false 3 | use GenServer 4 | 5 | alias IO.ANSI 6 | alias Display.{Failure, Intro, Notifications, ProgressBar} 7 | 8 | def start_link do 9 | GenServer.start_link(__MODULE__, %{clear_screen: true}, name: __MODULE__) 10 | end 11 | 12 | def init(args) do 13 | {:ok, args} 14 | end 15 | 16 | def disable_clear do 17 | GenServer.cast(__MODULE__, :disable_clear) 18 | end 19 | 20 | def handle_cast(:disable_clear, state) do 21 | {:noreply, %{state | clear_screen: false}} 22 | end 23 | 24 | def handle_call(:clear_screen, _from, %{clear_screen: true} = state) do 25 | (ANSI.clear() <> ANSI.home()) |> IO.puts() 26 | 27 | {:reply, :ok, state} 28 | end 29 | 30 | def handle_call(:clear_screen, _from, state) do 31 | {:reply, :ok, state} 32 | end 33 | 34 | def invalid_koan(koan, modules) do 35 | Notifications.invalid_koan(koan, modules) 36 | |> IO.puts() 37 | end 38 | 39 | def show_failure(failure, module, name) do 40 | format(failure, module, name) 41 | |> IO.puts() 42 | end 43 | 44 | def show_compile_error(error) do 45 | Failure.show_compile_error(error) 46 | |> IO.puts() 47 | end 48 | 49 | def congratulate do 50 | Notifications.congratulate() 51 | |> IO.puts() 52 | end 53 | 54 | def clear_screen do 55 | GenServer.call(__MODULE__, :clear_screen) 56 | end 57 | 58 | defp format(failure, module, name) do 59 | progress_bar = ProgressBar.progress_bar(Tracker.summarize()) 60 | progress_bar_underline = String.duplicate("-", String.length(progress_bar)) 61 | 62 | """ 63 | #{Intro.intro(module, Tracker.visited())} 64 | Now meditate upon #{format_module(module)} 65 | #{progress_bar} 66 | #{progress_bar_underline} 67 | #{name} 68 | #{Failure.format_failure(failure)} 69 | """ 70 | end 71 | 72 | defp format_module(module) do 73 | Module.split(module) |> List.last() 74 | end 75 | end 76 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Elixir Koans 2 | 3 | ![CI](https://github.com/elixirkoans/elixir-koans/actions/workflows/elixir.yml/badge.svg) 4 | 5 | 6 | Elixir koans is a fun way to get started with the elixir programming language. It is a tour 7 | of the most important features and idiomatic usage of the language. 8 | 9 | ### Prerequisites 10 | 11 | You need to have Elixir installed. Please refer to the [official guide](http://elixir-lang.org/install.html) for instructions. 12 | 13 | First, clone the repo from GitHub: 14 | 15 | ```sh 16 | $ git clone https://github.com/elixirkoans/elixir-koans.git 17 | $ cd elixir-koans/ 18 | ``` 19 | 20 | Next, fetch mix dependencies by running: 21 | 22 | ```sh 23 | $ mix deps.get 24 | ``` 25 | 26 | You might get prompted to install further dependencies. Reply "y". 27 | 28 | On Linux, you'll need to install `inotify-tools` to be able 29 | to use the autorunner in this project. 30 | 31 | ### Running 32 | 33 | With the dependencies installed, navigate to the root directory of this project and run: 34 | 35 | ```sh 36 | $ mix meditate 37 | ``` 38 | 39 | You should see the first failure. Open the corresponding file in your favourite text editor 40 | and fill in the blanks to make the koans pass one by one. 41 | The autorunner will give you feedback each time you save. 42 | 43 | 44 | If you want the autorunner to show you your previous results, run it with `--no-clear-screen` 45 | ```sh 46 | $ mix meditate --no-clear-screen 47 | ``` 48 | 49 | If you want to jump to a specific lesson, run it with `--koan=` 50 | ```sh 51 | $ mix meditate --koan=PatternMatching 52 | ``` 53 | 54 | Any typos on the koan name will show the complete list of koans, where you can pick any. 55 | 56 | 57 | ### Contributing 58 | 59 | We welcome contributions! If something does not make sense along the way or you feel 60 | like an important lesson is missing from the koans, feel free to fork the project 61 | and open a pull request. 62 | 63 | List of [contributors](CONTRIBUTORS.md). 64 | -------------------------------------------------------------------------------- /lib/koans/06_lists.ex: -------------------------------------------------------------------------------- 1 | defmodule Lists do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Lists" 6 | 7 | koan "We can see what is ahead" do 8 | assert List.first([1, 2, 3]) == ___ 9 | end 10 | 11 | koan "Checking what's trailing is also simple" do 12 | assert List.last([1, 2, 3]) == ___ 13 | end 14 | 15 | koan "Lists can store anything you throw at them" do 16 | assert [1, 2] ++ [:a, "b"] == ___ 17 | end 18 | 19 | koan "Things can evolve" do 20 | assert [1, 2, 3] -- [3] == ___ 21 | end 22 | 23 | koan "Evolution can have different forms" do 24 | assert List.delete([:a, :b, :c], :b) == ___ 25 | end 26 | 27 | koan "Precision is also valued" do 28 | assert List.delete_at([:a, :b, :c], 2) == ___ 29 | end 30 | 31 | koan "Replication is also possible" do 32 | assert List.duplicate("life", 3) == ___ 33 | end 34 | 35 | koan "Sometimes leveling the playing field is desired" do 36 | assert List.flatten([1, [2, 3], 4, [5]]) == ___ 37 | end 38 | 39 | koan "Order can also be specified for new members" do 40 | assert List.insert_at([1, 2, 3], 1, 4) == ___ 41 | end 42 | 43 | koan "We can replace things at specified positions" do 44 | assert List.replace_at([1, 2, 3], 0, 10) == ___ 45 | end 46 | 47 | koan "When a replacement cannot be found, the list remains the same" do 48 | assert List.replace_at([1, 2, 3], 10, 0) == ___ 49 | end 50 | 51 | koan "Order is bound by nature's laws" do 52 | assert List.insert_at([1, 2, 3], 10, 4) == ___ 53 | end 54 | 55 | koan "Sometimes it's faster to loop around back" do 56 | assert List.insert_at([1, 2, 3], -1, 4) == ___ 57 | end 58 | 59 | koan "We can also transform ourselves completely" do 60 | assert List.to_tuple([1, 2, 3]) == ___ 61 | end 62 | 63 | koan "Wrapping other values is a handy option" do 64 | assert List.wrap("value") == ___ 65 | end 66 | 67 | koan "Wrapping nothing produces a list of nothing" do 68 | assert List.wrap(nil) == ___ 69 | end 70 | 71 | koan "When there is already a list do not wrap it again" do 72 | assert List.wrap(["value"]) == ___ 73 | end 74 | end 75 | -------------------------------------------------------------------------------- /lib/koans/09_map_sets.ex: -------------------------------------------------------------------------------- 1 | defmodule MapSets do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "My name is Set, MapSet." 6 | 7 | @set MapSet.new([1, 2, 3, 4, 5]) 8 | 9 | koan "I do not allow duplication" do 10 | new_set = MapSet.new([1, 1, 2, 3, 3, 3]) 11 | 12 | assert MapSet.size(new_set) == ___ 13 | end 14 | 15 | def sorted?(set) do 16 | list = MapSet.to_list(set) 17 | sorted = Enum.sort(list) 18 | list == sorted 19 | end 20 | 21 | koan "You cannot depend on my order" do 22 | new_set = MapSet.new(1..33) 23 | assert sorted?(new_set) == ___ 24 | 25 | # Note: The number "33" is actually special here. Erlang uses a different 26 | # implementation for maps after 32 elements which does not maintain order. 27 | # http://stackoverflow.com/a/40408469 28 | 29 | # What do you think this answer to this assertion is? 30 | assert sorted?(@set) == ___ 31 | end 32 | 33 | koan "Does this value exist in the map set?" do 34 | assert MapSet.member?(@set, 3) == ___ 35 | end 36 | 37 | koan "I am merely another collection, but you can perform some operations on me" do 38 | new_set = MapSet.new(@set, fn x -> 3 * x end) 39 | 40 | assert MapSet.member?(new_set, 15) == ___ 41 | assert MapSet.member?(new_set, 1) == ___ 42 | end 43 | 44 | koan "Add this value into a map set" do 45 | modified_set = MapSet.put(@set, 6) 46 | 47 | assert MapSet.member?(modified_set, 6) == ___ 48 | end 49 | 50 | koan "Delete this value from the map set" do 51 | modified_set = MapSet.delete(@set, 1) 52 | 53 | assert MapSet.member?(modified_set, 1) == ___ 54 | end 55 | 56 | koan "Are these maps twins?" do 57 | new_set = MapSet.new([1, 2, 3]) 58 | 59 | assert MapSet.equal?(@set, new_set) == ___ 60 | end 61 | 62 | koan "I want only the common values in both sets" do 63 | intersection_set = MapSet.intersection(@set, MapSet.new([5, 6, 7])) 64 | 65 | assert MapSet.member?(intersection_set, 5) == ___ 66 | end 67 | 68 | koan "Unify my sets" do 69 | new_set = MapSet.union(@set, MapSet.new([1, 5, 6, 7])) 70 | 71 | assert MapSet.size(new_set) == ___ 72 | end 73 | 74 | koan "I want my set in a list" do 75 | assert MapSet.to_list(@set) == ___ 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /lib/runner.ex: -------------------------------------------------------------------------------- 1 | defmodule Runner do 2 | @moduledoc false 3 | use GenServer 4 | 5 | def koan?(koan) do 6 | case Code.ensure_loaded(koan) do 7 | {:module, _} -> Keyword.has_key?(koan.__info__(:functions), :all_koans) 8 | _ -> false 9 | end 10 | end 11 | 12 | def modules do 13 | {:ok, modules} = :application.get_key(:elixir_koans, :modules) 14 | 15 | modules 16 | |> Stream.map(&(&1.module_info |> get_in([:compile, :source]))) 17 | # Paths are charlists 18 | |> Stream.map(&to_string/1) 19 | |> Stream.zip(modules) 20 | |> Stream.filter(fn {_path, mod} -> koan?(mod) end) 21 | |> Stream.map(fn {path, mod} -> {path_to_number(path), mod} end) 22 | |> Enum.sort_by(fn {number, _mod} -> number end) 23 | |> Enum.map(fn {_number, mod} -> mod end) 24 | end 25 | 26 | @koan_path_pattern ~r/lib\/koans\/(\d+)_\w+.ex$/ 27 | def koan_path_pattern, do: @koan_path_pattern 28 | 29 | def path_to_number(path) do 30 | [_path, number] = Regex.run(@koan_path_pattern, path) 31 | String.to_integer(number) 32 | end 33 | 34 | def modules_to_run(start_module), do: Enum.drop_while(modules(), &(&1 != start_module)) 35 | 36 | def init(args) do 37 | {:ok, args} 38 | end 39 | 40 | def start_link do 41 | GenServer.start_link(__MODULE__, [], name: __MODULE__) 42 | end 43 | 44 | def handle_cast({:run, modules}, _) do 45 | flush() 46 | send(self(), :run_modules) 47 | {:noreply, modules} 48 | end 49 | 50 | def handle_info(:run_modules, []) do 51 | {:noreply, []} 52 | end 53 | 54 | def handle_info(:run_modules, [module | rest]) do 55 | Display.clear_screen() 56 | 57 | case run_module(module) do 58 | :passed -> 59 | send(self(), :run_modules) 60 | {:noreply, rest} 61 | 62 | _ -> 63 | {:noreply, []} 64 | end 65 | end 66 | 67 | def run(modules) do 68 | GenServer.cast(__MODULE__, {:run, modules}) 69 | end 70 | 71 | defp run_module(module) do 72 | module 73 | |> Execute.run_module(&track/3) 74 | |> display 75 | end 76 | 77 | defp track(:passed, module, koan), do: Tracker.completed(module, koan) 78 | defp track(_, _, _), do: nil 79 | 80 | defp display({:failed, error, module, name}) do 81 | Display.show_failure(error, module, name) 82 | :failed 83 | end 84 | 85 | defp display(_), do: :passed 86 | 87 | defp flush do 88 | receive do 89 | _ -> flush() 90 | after 91 | 0 -> :ok 92 | end 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /test/blanks_test.exs: -------------------------------------------------------------------------------- 1 | defmodule BlanksTest do 2 | use ExUnit.Case, async: true 3 | 4 | test "simple replacement" do 5 | ast = quote do: 1 + ___ 6 | 7 | assert Blanks.replace(ast, 37) == quote(do: 1 + 37) 8 | end 9 | 10 | test "Work with multiple different replacements" do 11 | [koan | _] = SampleKoan.all_koans() 12 | assert :ok == apply(SampleKoan, koan, [{:multiple, [3, 4]}]) 13 | end 14 | 15 | test "complex example" do 16 | ast = quote do: assert(true == ___) 17 | 18 | assert Blanks.replace(ast, true) == quote(do: assert(true == true)) 19 | end 20 | 21 | test "multiple arguments" do 22 | # credo:disable-for-next-line Credo.Check.Warning.OperationOnSameValues 23 | ast = quote do: assert(___ == ___) 24 | 25 | assert Blanks.replace(ast, [true, false]) == quote(do: assert(true == false)) 26 | end 27 | 28 | test "pins variables in assert_receive replacement" do 29 | ast = quote do: assert_receive(___) 30 | 31 | assert Blanks.replace(ast, Macro.var(:answer, __MODULE__)) == 32 | quote(do: assert_receive(^answer)) 33 | end 34 | 35 | test "does not pin values in assert_receive replacement" do 36 | ast = quote do: assert_receive(___) 37 | assert Blanks.replace(ast, :lolwat) == quote(do: assert_receive(:lolwat)) 38 | end 39 | 40 | test "counts simple blanks" do 41 | ast = quote do: 1 + ___ 42 | 43 | assert Blanks.count(ast) == 1 44 | end 45 | 46 | test "counts multiple blanks" do 47 | # credo:disable-for-next-line Credo.Check.Warning.OperationOnSameValues 48 | ast = quote do: assert(___ == ___) 49 | 50 | assert Blanks.count(ast) == 2 51 | end 52 | 53 | test "replaces whole line containing blank" do 54 | ast = 55 | quote do 56 | 1 + 2 57 | 2 + ___ 58 | end 59 | 60 | expected_result = 61 | quote do 62 | 1 + 2 63 | true 64 | end 65 | 66 | actual_result = Blanks.replace_line(ast, fn _ -> true end) 67 | 68 | assert actual_result == expected_result 69 | end 70 | 71 | test "replacement fn can access line" do 72 | ast = 73 | quote do 74 | 1 + 2 75 | 2 + ___ 76 | end 77 | 78 | expected_result = 79 | quote do 80 | 1 + 2 81 | some_fun(2 + ___) 82 | end 83 | 84 | actual_result = 85 | Blanks.replace_line(ast, fn line -> 86 | quote do: some_fun(unquote(line)) 87 | end) 88 | 89 | assert actual_result == expected_result 90 | end 91 | end 92 | -------------------------------------------------------------------------------- /lib/koans.ex: -------------------------------------------------------------------------------- 1 | defmodule Koans do 2 | @moduledoc false 3 | defp valid_name(name) do 4 | Regex.match?(~r/([A-Z]|\.\.\.).+/, name) 5 | end 6 | 7 | defmacro koan(name, do: body) do 8 | if not valid_name(name) do 9 | raise "Name does not start with a capital letter: #{name}" 10 | end 11 | 12 | compiled_name = String.to_atom(name) 13 | number_of_args = Blanks.count(body) 14 | compiled_body = Blanks.replace_line(body, &blank_line_replacement/1) 15 | 16 | quote do 17 | @koans unquote(compiled_name) 18 | 19 | generate_test_method(unquote(compiled_name), unquote(number_of_args), unquote(body)) 20 | 21 | def unquote(compiled_name)() do 22 | unquote(compiled_body) 23 | :ok 24 | rescue 25 | e -> {:error, __STACKTRACE__, e} 26 | end 27 | end 28 | end 29 | 30 | defmacro generate_test_method(_name, 0, _body), do: false 31 | 32 | defmacro generate_test_method(name, 1, body) do 33 | single_var = Blanks.replace(body, Macro.var(:answer, __MODULE__)) 34 | 35 | quote do 36 | def unquote(name)(answer) do 37 | unquote(single_var) 38 | :ok 39 | rescue 40 | e -> {:error, __STACKTRACE__, e} 41 | end 42 | end 43 | end 44 | 45 | defmacro generate_test_method(name, number_of_args, body) do 46 | answer_vars = 47 | for id <- 1..number_of_args, do: Macro.var(String.to_atom("answer#{id}"), __MODULE__) 48 | 49 | multi_var = Blanks.replace(body, answer_vars) 50 | 51 | quote do 52 | def unquote(name)({:multiple, unquote(answer_vars)}) do 53 | unquote(multi_var) 54 | :ok 55 | rescue 56 | e -> {:error, __STACKTRACE__, e} 57 | end 58 | end 59 | end 60 | 61 | defp blank_line_replacement({:assert, _meta, [expr]}) do 62 | code = Macro.escape(expr) 63 | quote do: raise(ExUnit.AssertionError, expr: unquote(code)) 64 | end 65 | 66 | defp blank_line_replacement(line) do 67 | code = Macro.escape(line) 68 | quote do: raise(ExUnit.AssertionError, expr: unquote(code)) 69 | end 70 | 71 | defmacro __using__(_opts) do 72 | quote do 73 | @compile :nowarn_unused_vars 74 | Module.register_attribute(__MODULE__, :koans, accumulate: true) 75 | 76 | require ExUnit.Assertions 77 | import ExUnit.Assertions 78 | import Koans 79 | 80 | @before_compile Koans 81 | end 82 | end 83 | 84 | defmacro __before_compile__(env) do 85 | koans = koans(env) 86 | 87 | quote do 88 | def all_koans do 89 | unquote(koans) 90 | end 91 | 92 | def intro, do: @intro 93 | end 94 | end 95 | 96 | defp koans(env) do 97 | env.module 98 | |> Module.get_attribute(:koans) 99 | |> Enum.reverse() 100 | end 101 | end 102 | -------------------------------------------------------------------------------- /lib/koans/03_numbers.ex: -------------------------------------------------------------------------------- 1 | defmodule Numbers do 2 | @moduledoc false 3 | require Integer 4 | use Koans 5 | 6 | @intro "Why is the number six so scared? Because seven eight nine!\nWe should get to know numbers a bit more!" 7 | 8 | koan "Is an integer equal to its float equivalent?" do 9 | assert 1 == 1.0 == ___ 10 | end 11 | 12 | koan "Is an integer threequal to its float equivalent?" do 13 | assert 1 === 1.0 == ___ 14 | end 15 | 16 | koan "Revisit division with threequal" do 17 | assert 2 / 2 === ___ 18 | end 19 | 20 | koan "Another way to divide" do 21 | assert div(5, 2) == ___ 22 | end 23 | 24 | koan "What remains or: The Case of the Missing Modulo Operator (%)" do 25 | assert rem(5, 2) == ___ 26 | end 27 | 28 | koan "Other math operators may produce this" do 29 | assert 2 * 2 === ___ 30 | end 31 | 32 | koan "Or other math operators may produce this" do 33 | assert 2 * 2.0 === ___ 34 | end 35 | 36 | koan "Two ways to round, are they exactly the same?" do 37 | assert Float.round(1.2) === round(1.2) == ___ 38 | end 39 | 40 | koan "Release the decimals into the void" do 41 | assert trunc(5.6) === ___ 42 | end 43 | 44 | koan "Are you odd?" do 45 | assert Integer.is_odd(3) == ___ 46 | end 47 | 48 | koan "Actually you might be even" do 49 | assert Integer.is_even(4) == ___ 50 | end 51 | 52 | koan "Let's grab the individual digits in a list" do 53 | individual_digits = Integer.digits(58_127) 54 | assert individual_digits == ___ 55 | end 56 | 57 | koan "Oh no! I need it back together" do 58 | number = Integer.undigits([1, 2, 3, 4]) 59 | 60 | assert number == ___ 61 | end 62 | 63 | koan "Actually I want my number as a string" do 64 | string_digit = Integer.to_string(1234) 65 | 66 | assert string_digit == ___ 67 | end 68 | 69 | koan "The meaning of life in hexadecimal is 2A!" do 70 | assert Integer.parse("2A", 16) == {___, ""} 71 | end 72 | 73 | koan "The remaining unparsable part is also returned" do 74 | assert Integer.parse("5 years") == {5, ___} 75 | end 76 | 77 | koan "What if you parse a floating point value as an integer?" do 78 | assert Integer.parse("1.2") == {___, ___} 79 | end 80 | 81 | koan "Just want to parse to a float" do 82 | assert Float.parse("34.5") == {___, ""} 83 | end 84 | 85 | koan "Hmm, I want to parse this but it has some strings" do 86 | assert Float.parse("1.5 million dollars") == {___, " million dollars"} 87 | end 88 | 89 | koan "I don't want this decimal point, let's round up" do 90 | assert Float.ceil(34.25) === ___ 91 | end 92 | 93 | koan "OK, I only want it to 1 decimal place" do 94 | assert Float.ceil(34.25, 1) === ___ 95 | end 96 | 97 | koan "Rounding down is what I need" do 98 | assert Float.floor(99.99) === ___ 99 | end 100 | 101 | koan "Rounding down to 2 decimal places" do 102 | assert Float.floor(12.345, 2) === ___ 103 | end 104 | 105 | koan "Round the number up or down for me" do 106 | assert Float.round(5.5) === ___ 107 | assert Float.round(5.4) === ___ 108 | assert Float.round(8.94, 1) === ___ 109 | assert Float.round(-5.5674, 3) === ___ 110 | end 111 | 112 | koan "I want the first and last in the range" do 113 | first..last//_step = Range.new(1, 10) 114 | 115 | assert first == ___ 116 | assert last == ___ 117 | end 118 | 119 | koan "Does my number exist in the range?" do 120 | range = Range.new(1, 10) 121 | 122 | assert 4 in range == ___ 123 | assert 10 in range == ___ 124 | assert 0 in range == ___ 125 | end 126 | 127 | def range?(%Range{}), do: true 128 | def range?(_), do: false 129 | 130 | koan "Is this a range?" do 131 | assert range?(1..10) == ___ 132 | assert range?(0) == ___ 133 | end 134 | end 135 | -------------------------------------------------------------------------------- /lib/koans/14_enums.ex: -------------------------------------------------------------------------------- 1 | defmodule Enums do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Enums" 6 | 7 | koan "Knowing how many elements are in a list is important for book-keeping" do 8 | assert Enum.count([1, 2, 3]) == ___ 9 | end 10 | 11 | koan "Counting is similar to length" do 12 | assert length([1, 2, 3]) == ___ 13 | end 14 | 15 | koan "But it allows you to count certain elements" do 16 | assert Enum.count([1, 2, 3], &(&1 == 2)) == ___ 17 | end 18 | 19 | koan "Depending on the type, it counts pairs while length does not" do 20 | map = %{a: :foo, b: :bar} 21 | assert Enum.count(map) == ___ 22 | assert_raise ___, fn -> length(map) end 23 | end 24 | 25 | def less_than_five?(n), do: n < 5 26 | 27 | koan "Elements can have a lot in common" do 28 | assert Enum.all?([1, 2, 3], &less_than_five?/1) == ___ 29 | assert Enum.all?([4, 6, 8], &less_than_five?/1) == ___ 30 | end 31 | 32 | def even?(n), do: rem(n, 2) == 0 33 | 34 | koan "Sometimes you just want to know if there are any elements fulfilling a condition" do 35 | assert Enum.any?([1, 2, 3], &even?/1) == ___ 36 | assert Enum.any?([1, 3, 5], &even?/1) == ___ 37 | end 38 | 39 | koan "Sometimes you just want to know if an element is part of the party" do 40 | input = [1, 2, 3] 41 | assert Enum.member?(input, 1) == ___ 42 | assert Enum.member?(input, 30) == ___ 43 | end 44 | 45 | def multiply_by_ten(n), do: 10 * n 46 | 47 | koan "Mapping converts each element of a list by running some function with it" do 48 | assert Enum.map([1, 2, 3], &multiply_by_ten/1) == ___ 49 | end 50 | 51 | def odd?(n), do: rem(n, 2) == 1 52 | 53 | koan "Filter allows you to only keep what you really care about" do 54 | assert Enum.filter([1, 2, 3], &odd?/1) == ___ 55 | end 56 | 57 | koan "Reject will help you throw out unwanted cruft" do 58 | assert Enum.reject([1, 2, 3], &odd?/1) == ___ 59 | end 60 | 61 | koan "You three there, follow me!" do 62 | assert Enum.take([1, 2, 3, 4, 5], 3) == ___ 63 | end 64 | 65 | koan "You can ask for a lot, but Enum won't hand you more than you give" do 66 | assert Enum.take([1, 2, 3, 4, 5], 10) == ___ 67 | end 68 | 69 | koan "Just like taking, you can also drop elements" do 70 | assert Enum.drop([-1, 0, 1, 2, 3], 2) == ___ 71 | end 72 | 73 | koan "Zip-up in pairs!" do 74 | letters = [:a, :b, :c] 75 | numbers = [1, 2, 3] 76 | assert Enum.zip(letters, numbers) == ___ 77 | end 78 | 79 | koan "When you want to find that one pesky element, it returns the first" do 80 | assert Enum.find([1, 2, 3, 4], &even?/1) == ___ 81 | end 82 | 83 | def divisible_by_five?(n), do: rem(n, 5) == 0 84 | 85 | koan "...but you don't quite find it..." do 86 | assert Enum.find([1, 2, 3], &divisible_by_five?/1) == ___ 87 | end 88 | 89 | koan "...you can settle for a consolation prize" do 90 | assert Enum.find([1, 2, 3], :no_such_element, &divisible_by_five?/1) == ___ 91 | end 92 | 93 | koan "Collapse an entire list of elements down to a single one by repeating a function." do 94 | assert Enum.reduce([1, 2, 3], 0, fn element, accumulator -> element + accumulator end) == ___ 95 | end 96 | 97 | koan "Enum.chunk_every splits lists into smaller lists of fixed size" do 98 | assert Enum.chunk_every([1, 2, 3, 4, 5, 6], 2) == ___ 99 | assert Enum.chunk_every([1, 2, 3, 4, 5], 3) == ___ 100 | end 101 | 102 | koan "Enum.flat_map transforms and flattens in one step" do 103 | result = 104 | [1, 2, 3] 105 | |> Enum.flat_map(&[&1, &1 * 10]) 106 | 107 | assert result == ___ 108 | end 109 | 110 | koan "Enum.group_by organizes elements by a grouping function" do 111 | words = ["apple", "banana", "cherry", "apricot", "blueberry"] 112 | grouped = Enum.group_by(words, &String.first/1) 113 | 114 | assert grouped["a"] == ___ 115 | assert grouped["b"] == ___ 116 | end 117 | 118 | koan "Stream provides lazy enumeration for large datasets" do 119 | # Streams are lazy - they don't execute until you call Enum on them 120 | stream = 121 | 1..1_000_000 122 | |> Stream.filter(&even?/1) 123 | |> Stream.map(&(&1 * 2)) 124 | |> Stream.take(3) 125 | 126 | # Nothing has been computed yet! 127 | result = Enum.to_list(stream) 128 | assert result == ___ 129 | end 130 | end 131 | -------------------------------------------------------------------------------- /lib/koans/21_control_flow.ex: -------------------------------------------------------------------------------- 1 | # credo:disable-for-this-file Credo.Check.Refactor.UnlessWithElse 2 | # credo:disable-for-this-file Credo.Check.Refactor.CondStatements 3 | defmodule ControlFlow do 4 | @moduledoc false 5 | use Koans 6 | 7 | @intro "Control Flow - Making decisions and choosing paths" 8 | 9 | koan "If statements evaluate conditions" do 10 | result = if true, do: "yes", else: "no" 11 | assert result == ___ 12 | end 13 | 14 | koan "If can be written in block form" do 15 | result = 16 | if 1 + 1 == 2 do 17 | "math works" 18 | else 19 | "math is broken" 20 | end 21 | 22 | assert result == ___ 23 | end 24 | 25 | koan "Unless is the opposite of if" do 26 | result = unless false, do: "will execute", else: "will not execute" 27 | assert result == ___ 28 | end 29 | 30 | koan "Nil and false are falsy, everything else is truthy" do 31 | assert if(nil, do: "truthy", else: "falsy") == ___ 32 | assert if(false, do: "truthy", else: "falsy") == ___ 33 | assert if(0, do: "truthy", else: "falsy") == ___ 34 | assert if("", do: "truthy", else: "falsy") == ___ 35 | assert if([], do: "truthy", else: "falsy") == ___ 36 | end 37 | 38 | koan "Case matches against patterns" do 39 | result = 40 | case {1, 2, 3} do 41 | {4, 5, 6} -> "no match" 42 | {1, x, 3} -> "matched with x = #{x}" 43 | end 44 | 45 | assert result == ___ 46 | end 47 | 48 | koan "Case can have multiple clauses with different patterns" do 49 | check_number = fn x -> 50 | case x do 51 | 0 -> "zero" 52 | n when n > 0 -> "positive" 53 | n when n < 0 -> "negative" 54 | end 55 | end 56 | 57 | assert check_number.(5) == ___ 58 | assert check_number.(0) == ___ 59 | assert check_number.(-3) == ___ 60 | end 61 | 62 | koan "Case clauses are tried in order until one matches" do 63 | check_list = fn list -> 64 | case list do 65 | [] -> "empty" 66 | [_] -> "one element" 67 | [_, _] -> "two elements" 68 | _ -> "many elements" 69 | end 70 | end 71 | 72 | assert check_list.([]) == ___ 73 | assert check_list.([:a]) == ___ 74 | assert check_list.([:a, :b]) == ___ 75 | assert check_list.([:a, :b, :c, :d]) == ___ 76 | end 77 | 78 | koan "Cond evaluates conditions until one is truthy" do 79 | temperature = 25 80 | 81 | weather = 82 | cond do 83 | temperature < 0 -> "freezing" 84 | temperature < 10 -> "cold" 85 | temperature < 25 -> "cool" 86 | temperature < 30 -> "warm" 87 | true -> "hot" 88 | end 89 | 90 | assert weather == ___ 91 | end 92 | 93 | koan "Cond requires at least one clause to be true" do 94 | safe_divide = fn x, y -> 95 | cond do 96 | y == 0 -> {:error, "division by zero"} 97 | true -> {:ok, x / y} 98 | end 99 | end 100 | 101 | assert safe_divide.(10, 2) == ___ 102 | assert safe_divide.(10, 0) == ___ 103 | end 104 | 105 | koan "Case can destructure complex patterns" do 106 | parse_response = fn response -> 107 | case response do 108 | {:ok, %{status: 200, body: body}} -> "Success: #{body}" 109 | {:ok, %{status: status}} when status >= 400 -> "Client error: #{status}" 110 | {:ok, %{status: status}} when status >= 500 -> "Server error: #{status}" 111 | {:error, reason} -> "Request failed: #{reason}" 112 | end 113 | end 114 | 115 | assert parse_response.({:ok, %{status: 200, body: "Hello"}}) == ___ 116 | assert parse_response.({:ok, %{status: 404}}) == ___ 117 | assert parse_response.({:error, :timeout}) == ___ 118 | end 119 | 120 | koan "Guards in case can use complex expressions" do 121 | categorize = fn number -> 122 | case number do 123 | n when is_integer(n) and n > 0 and rem(n, 2) == 0 -> "positive even integer" 124 | n when is_integer(n) and n > 0 and rem(n, 2) == 1 -> "positive odd integer" 125 | n when is_integer(n) and n < 0 -> "negative integer" 126 | n when is_float(n) -> "float" 127 | _ -> "other" 128 | end 129 | end 130 | 131 | assert categorize.(4) == ___ 132 | assert categorize.(3) == ___ 133 | assert categorize.(-5) == ___ 134 | assert categorize.(3.14) == ___ 135 | assert categorize.("hello") == ___ 136 | end 137 | 138 | koan "Multiple conditions can be checked in sequence" do 139 | process_user = fn user -> 140 | if user.active do 141 | if user.verified do 142 | if user.premium do 143 | "premium verified active user" 144 | else 145 | "verified active user" 146 | end 147 | else 148 | "unverified active user" 149 | end 150 | else 151 | "inactive user" 152 | end 153 | end 154 | 155 | user = %{active: true, verified: true, premium: false} 156 | assert process_user.(user) == ___ 157 | end 158 | end 159 | -------------------------------------------------------------------------------- /lib/koans/13_functions.ex: -------------------------------------------------------------------------------- 1 | defmodule Functions do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Functions" 6 | 7 | def greet(name) do 8 | "Hello, #{name}!" 9 | end 10 | 11 | koan "Functions map arguments to outputs" do 12 | assert greet("World") == ___ 13 | end 14 | 15 | def multiply(a, b), do: a * b 16 | 17 | koan "Single line functions are cool, but mind the comma and the colon!" do 18 | assert 6 == multiply(2, ___) 19 | end 20 | 21 | def first(foo, bar), do: "#{foo} and #{bar}" 22 | def first(foo), do: "Only #{foo}" 23 | 24 | koan "Functions with the same name are distinguished by the number of arguments they take" do 25 | assert first("One", "Two") == ___ 26 | assert first("One") == ___ 27 | end 28 | 29 | def repeat_again(message, times \\ 3) do 30 | String.duplicate(message, times) 31 | end 32 | 33 | koan "Functions can have default argument values" do 34 | assert repeat_again("Hello ") == ___ 35 | assert repeat_again("Hello ", 2) == ___ 36 | end 37 | 38 | def sum_up(thing) when is_list(thing), do: :entire_list 39 | def sum_up(_thing), do: :single_thing 40 | 41 | koan "Functions can have guard expressions" do 42 | assert sum_up([1, 2, 3]) == ___ 43 | assert sum_up(1) == ___ 44 | end 45 | 46 | def bigger(a, b) when a > b, do: "#{a} is bigger than #{b}" 47 | def bigger(a, b) when a <= b, do: "#{a} is not bigger than #{b}" 48 | 49 | koan "Intricate guards are possible, but be mindful of the reader" do 50 | assert bigger(10, 5) == ___ 51 | assert bigger(4, 27) == ___ 52 | end 53 | 54 | def get_number(0), do: "The number was zero" 55 | def get_number(number), do: "The number was #{number}" 56 | 57 | koan "For simpler cases, pattern matching is effective" do 58 | assert get_number(0) == ___ 59 | assert get_number(5) == ___ 60 | end 61 | 62 | koan "Little anonymous functions are common, and called with a dot" do 63 | multiply = fn a, b -> a * b end 64 | assert multiply.(2, 3) == ___ 65 | end 66 | 67 | koan "You can even go shorter, by using capture syntax `&()` and positional arguments" do 68 | multiply = &(&1 * &2) 69 | assert multiply.(2, 3) == ___ 70 | end 71 | 72 | koan "Prefix a string with & to build a simple anonymous greet function" do 73 | greet = &"Hi, #{&1}!" 74 | assert greet.("Foo") == ___ 75 | end 76 | 77 | koan "You can build anonymous functions out of any elixir expression by prefixing it with &" do 78 | three_times = &[&1, &1, &1] 79 | assert three_times.("foo") == ___ 80 | end 81 | 82 | koan "You can use pattern matching to define multiple cases for anonymous functions" do 83 | inspirational_quote = fn 84 | {:ok, result} -> "Success is #{result}" 85 | {:error, reason} -> "You just lost #{reason}" 86 | end 87 | 88 | assert inspirational_quote.({:ok, "no accident"}) == ___ 89 | assert inspirational_quote.({:error, "the game"}) == ___ 90 | end 91 | 92 | def times_five_and_then(number, fun), do: fun.(number * 5) 93 | def square(number), do: number * number 94 | 95 | koan "You can pass functions around as arguments. Place an '&' before the name and state the arity" do 96 | assert times_five_and_then(2, &square/1) == ___ 97 | end 98 | 99 | koan "The '&' operation is not needed for anonymous functions" do 100 | cube = fn number -> number * number * number end 101 | assert times_five_and_then(2, cube) == ___ 102 | end 103 | 104 | koan "The result of a function can be piped into another function as its first argument" do 105 | result = 106 | "full-name" 107 | |> String.split("-") 108 | |> Enum.map_join(" ", &String.capitalize/1) 109 | 110 | assert result == ___ 111 | end 112 | 113 | koan "Pipes make data transformation pipelines readable" do 114 | numbers = [1, 2, 3, 4, 5] 115 | 116 | result = 117 | numbers 118 | |> Enum.filter(&(&1 > 2)) 119 | |> Enum.map(&(&1 * 2)) 120 | |> Enum.sum() 121 | 122 | assert result == ___ 123 | 124 | user_input = " Hello World " 125 | 126 | cleaned = 127 | user_input 128 | |> String.trim() 129 | |> String.downcase() 130 | |> String.replace(" ", "_") 131 | 132 | assert cleaned == ___ 133 | end 134 | 135 | koan "Conveniently keyword lists can be used for function options" do 136 | transform = fn str, opts -> 137 | if opts[:upcase] do 138 | String.upcase(str) 139 | else 140 | str 141 | end 142 | end 143 | 144 | assert transform.("good", upcase: true) == ___ 145 | assert transform.("good", upcase: false) == ___ 146 | end 147 | 148 | koan "Anonymous functions can use the & capture syntax for very concise definitions" do 149 | add_one = &(&1 + 1) 150 | multiply_by_two = &(&1 * 2) 151 | 152 | result = 5 |> add_one.() |> multiply_by_two.() 153 | assert result == ___ 154 | 155 | # You can also capture existing functions 156 | string_length = &String.length/1 157 | assert string_length.("hello") == ___ 158 | end 159 | end 160 | -------------------------------------------------------------------------------- /lib/koans/15_processes.ex: -------------------------------------------------------------------------------- 1 | defmodule Processes do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Processes" 6 | 7 | koan "You are a process" do 8 | assert Process.alive?(self()) == ___ 9 | end 10 | 11 | koan "You can ask a process to introduce itself" do 12 | information = Process.info(self()) 13 | 14 | assert information[:status] == ___ 15 | end 16 | 17 | koan "Processes are referenced by their process ID (pid)" do 18 | assert is_pid(self()) == ___ 19 | end 20 | 21 | koan "New processes are spawned functions" do 22 | value = 23 | spawn(fn -> 24 | receive do 25 | end 26 | end) 27 | 28 | assert is_pid(value) == ___ 29 | end 30 | 31 | koan "Processes die when their function exits" do 32 | fast_process = spawn(fn -> :timer.sleep(10) end) 33 | slow_process = spawn(fn -> :timer.sleep(1000) end) 34 | 35 | # All spawned functions are executed concurrently with the current process. 36 | # You check back on slow_process and fast_process 50ms later. Let's 37 | # see if they are still alive! 38 | :timer.sleep(50) 39 | 40 | assert Process.alive?(fast_process) == ___ 41 | assert Process.alive?(slow_process) == ___ 42 | end 43 | 44 | koan "Processes can send and receive messages" do 45 | send(self(), "hola!") 46 | 47 | receive do 48 | msg -> assert msg == ___ 49 | end 50 | end 51 | 52 | koan "A process will wait forever for a message" do 53 | wait_forever = fn -> 54 | receive do 55 | end 56 | end 57 | 58 | pid = spawn(wait_forever) 59 | 60 | assert Process.alive?(pid) == ___ 61 | end 62 | 63 | koan "Received messages are queued, first in first out" do 64 | send(self(), "hola!") 65 | send(self(), "como se llama?") 66 | 67 | first_message = 68 | receive do 69 | message -> message 70 | end 71 | 72 | second_message = 73 | receive do 74 | message -> message 75 | end 76 | 77 | assert first_message == ___ 78 | assert second_message == ___ 79 | end 80 | 81 | koan "A common pattern is to include the sender in the message, so that it can reply" do 82 | greeter = fn -> 83 | receive do 84 | {:hello, sender} -> send(sender, :how_are_you?) 85 | end 86 | end 87 | 88 | pid = spawn(greeter) 89 | 90 | send(pid, {:hello, self()}) 91 | 92 | # ms 93 | timeout = 100 94 | 95 | failure_message = 96 | "Sorry, I didn't get the right message. Look at the message that is sent back very closely, and try again" 97 | 98 | assert_receive ___, timeout, failure_message 99 | end 100 | 101 | def yelling_echo_loop do 102 | receive do 103 | {caller, value} -> 104 | send(caller, String.upcase(value)) 105 | yelling_echo_loop() 106 | end 107 | end 108 | 109 | koan "Use tail recursion to receive multiple messages" do 110 | pid = spawn_link(&yelling_echo_loop/0) 111 | 112 | send(pid, {self(), "o"}) 113 | assert_receive ___ 114 | 115 | send(pid, {self(), "hai"}) 116 | assert_receive ___ 117 | end 118 | 119 | def state(value) do 120 | receive do 121 | {caller, :get} -> 122 | send(caller, value) 123 | state(value) 124 | 125 | {caller, :set, new_value} -> 126 | state(new_value) 127 | end 128 | end 129 | 130 | koan "Processes can be used to hold state" do 131 | initial_state = "foo" 132 | 133 | pid = 134 | spawn(fn -> 135 | state(initial_state) 136 | end) 137 | 138 | send(pid, {self(), :get}) 139 | assert_receive ___ 140 | 141 | send(pid, {self(), :set, "bar"}) 142 | send(pid, {self(), :get}) 143 | assert_receive ___ 144 | end 145 | 146 | koan "Waiting for a message can get boring" do 147 | parent = self() 148 | 149 | spawn(fn -> 150 | receive do 151 | after 152 | 5 -> send(parent, {:waited_too_long, "I am impatient"}) 153 | end 154 | end) 155 | 156 | assert_receive ___ 157 | end 158 | 159 | koan "Trapping will allow you to react to someone terminating the process" do 160 | parent = self() 161 | 162 | pid = 163 | spawn(fn -> 164 | Process.flag(:trap_exit, true) 165 | send(parent, :ready) 166 | 167 | receive do 168 | {:EXIT, _pid, reason} -> send(parent, {:exited, reason}) 169 | end 170 | end) 171 | 172 | receive do 173 | :ready -> true 174 | end 175 | 176 | Process.exit(pid, :random_reason) 177 | 178 | assert_receive ___ 179 | end 180 | 181 | koan "Parent processes can trap exits for children they are linked to" do 182 | Process.flag(:trap_exit, true) 183 | spawn_link(fn -> Process.exit(self(), :normal) end) 184 | 185 | assert_receive {:EXIT, _pid, ___} 186 | end 187 | 188 | koan "If you monitor your children, you'll be automatically informed of their departure" do 189 | spawn_monitor(fn -> Process.exit(self(), :normal) end) 190 | 191 | assert_receive {:DOWN, _ref, :process, _pid, ___} 192 | end 193 | end 194 | -------------------------------------------------------------------------------- /lib/koans/12_pattern_matching.ex: -------------------------------------------------------------------------------- 1 | defmodule PatternMatching do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "PatternMatching" 6 | 7 | koan "One matches one" do 8 | assert match?(1, ___) 9 | end 10 | 11 | koan "Patterns can be used to pull things apart" do 12 | [head | tail] = [1, 2, 3, 4] 13 | 14 | assert head == ___ 15 | assert tail == ___ 16 | end 17 | 18 | koan "And then put them back together" do 19 | head = 1 20 | tail = [2, 3, 4] 21 | 22 | assert ___ == [head | tail] 23 | end 24 | 25 | koan "Some values can be ignored" do 26 | [_first, _second, third, _fourth] = [1, 2, 3, 4] 27 | 28 | assert third == ___ 29 | end 30 | 31 | koan "Strings come apart just as easily" do 32 | "Shopping list: " <> items = "Shopping list: eggs, milk" 33 | 34 | assert items == ___ 35 | end 36 | 37 | koan "Maps support partial pattern matching" do 38 | %{make: make} = %{type: "car", year: 2016, make: "Honda", color: "black"} 39 | 40 | assert make == ___ 41 | end 42 | 43 | koan "Lists must match exactly" do 44 | assert_raise ___, fn -> 45 | [a, b] = [1, 2, 3] 46 | end 47 | end 48 | 49 | koan "So must keyword lists" do 50 | kw_list = [type: "car", year: 2016, make: "Honda"] 51 | [_type | [_year | [tuple]]] = kw_list 52 | assert tuple == {___, ___} 53 | end 54 | 55 | koan "The pattern can make assertions about what it expects" do 56 | assert match?([1, _second, _third], ___) 57 | end 58 | 59 | def make_noise(%{type: "cat"}), do: "Meow" 60 | def make_noise(%{type: "dog"}), do: "Woof" 61 | def make_noise(_anything), do: "Eh?" 62 | 63 | koan "Functions perform pattern matching on their arguments" do 64 | cat = %{type: "cat"} 65 | dog = %{type: "dog"} 66 | snake = %{type: "snake"} 67 | 68 | assert make_noise(cat) == ___ 69 | assert make_noise(dog) == ___ 70 | assert make_noise(snake) == ___ 71 | end 72 | 73 | koan "And they will only run the code that matches the argument" do 74 | name = fn 75 | "duck" -> "Donald" 76 | "mouse" -> "Mickey" 77 | _other -> "I need a name!" 78 | end 79 | 80 | assert name.("mouse") == ___ 81 | assert name.("duck") == ___ 82 | assert name.("donkey") == ___ 83 | end 84 | 85 | koan "Errors are shaped differently than successful results" do 86 | dog = %{type: "barking"} 87 | 88 | type = 89 | case Map.fetch(dog, :type) do 90 | {:ok, value} -> value 91 | :error -> "not present" 92 | end 93 | 94 | assert type == ___ 95 | end 96 | 97 | defmodule Animal do 98 | @moduledoc false 99 | defstruct [:kind, :name] 100 | end 101 | 102 | koan "You can pattern match into the fields of a struct" do 103 | %Animal{name: name} = %Animal{kind: "dog", name: "Max"} 104 | assert name == ___ 105 | end 106 | 107 | defmodule Plane do 108 | @moduledoc false 109 | defstruct passengers: 0, maker: :boeing 110 | end 111 | 112 | def plane?(%Plane{}), do: true 113 | def plane?(_), do: false 114 | 115 | koan "...or onto the type of the struct itself" do 116 | assert plane?(%Plane{passengers: 417, maker: :boeing}) == ___ 117 | assert plane?(%Animal{}) == ___ 118 | end 119 | 120 | koan "Structs will even match with a regular map" do 121 | %{name: name} = %Animal{kind: "dog", name: "Max"} 122 | assert name == ___ 123 | end 124 | 125 | koan "A value can be bound to a variable" do 126 | a = 1 127 | assert a == ___ 128 | end 129 | 130 | koan "A variable can be rebound" do 131 | a = 1 132 | a = 2 133 | assert a == ___ 134 | end 135 | 136 | koan "A variable can be pinned to use its value when matching instead of binding to a new value" do 137 | pinned_variable = 1 138 | 139 | example = fn 140 | ^pinned_variable -> "The number One" 141 | 2 -> "The number Two" 142 | number -> "The number #{number}" 143 | end 144 | 145 | assert example.(1) == ___ 146 | assert example.(2) == ___ 147 | assert example.(3) == ___ 148 | end 149 | 150 | koan "Pinning works anywhere one would match, including 'case'" do 151 | pinned_variable = 1 152 | 153 | result = 154 | case 1 do 155 | ^pinned_variable -> "same" 156 | other -> "different #{other}" 157 | end 158 | 159 | assert result == ___ 160 | end 161 | 162 | koan "Trying to rebind a pinned variable will result in an error" do 163 | a = 1 164 | 165 | assert_raise MatchError, fn -> 166 | ^a = ___ 167 | end 168 | end 169 | 170 | koan "Pattern matching works with nested data structures" do 171 | user = %{ 172 | profile: %{ 173 | personal: %{name: "Alice", age: 30}, 174 | settings: %{theme: "dark", notifications: true} 175 | } 176 | } 177 | 178 | %{profile: %{personal: %{age: age}, settings: %{theme: theme}}} = user 179 | assert age == ___ 180 | assert theme == ___ 181 | end 182 | 183 | koan "Lists can be pattern matched with head and tail" do 184 | numbers = [1, 2, 3, 4, 5] 185 | 186 | [first, second | rest] = numbers 187 | assert first == ___ 188 | assert second == ___ 189 | assert rest == ___ 190 | 191 | [head | _tail] = numbers 192 | assert head == ___ 193 | end 194 | 195 | koan "Pattern matching can extract values from function return tuples" do 196 | divide = fn 197 | _, 0 -> {:error, :division_by_zero} 198 | x, y -> {:ok, x / y} 199 | end 200 | 201 | {:ok, result} = divide.(10, 2) 202 | assert result == ___ 203 | 204 | {:error, reason} = divide.(10, 0) 205 | assert reason == ___ 206 | end 207 | end 208 | -------------------------------------------------------------------------------- /lib/koans/22_error_handling.ex: -------------------------------------------------------------------------------- 1 | defmodule ErrorHandling do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "Error Handling - Dealing gracefully with things that go wrong" 6 | 7 | koan "Result tuples are a common pattern for success and failure" do 8 | parse_number = fn string -> 9 | case Integer.parse(string) do 10 | {number, ""} -> {:ok, number} 11 | _ -> {:error, :invalid_format} 12 | end 13 | end 14 | 15 | assert parse_number.("123") == ___ 16 | assert parse_number.("abc") == ___ 17 | end 18 | 19 | koan "Pattern matching makes error handling elegant" do 20 | divide = fn x, y -> 21 | case y do 22 | 0 -> {:error, :division_by_zero} 23 | _ -> {:ok, x / y} 24 | end 25 | end 26 | 27 | result = 28 | case divide.(10, 2) do 29 | {:ok, value} -> "Result: #{value}" 30 | {:error, reason} -> "Error: #{reason}" 31 | end 32 | 33 | assert result == ___ 34 | end 35 | 36 | koan "Try-rescue catches runtime exceptions" do 37 | result = 38 | try do 39 | 10 / 0 40 | rescue 41 | ArithmeticError -> "Cannot divide by zero!" 42 | end 43 | 44 | assert result == ___ 45 | end 46 | 47 | koan "Try-rescue can catch specific exception types" do 48 | safe_list_access = fn list, index -> 49 | try do 50 | {:ok, Enum.at(list, index)} 51 | rescue 52 | FunctionClauseError -> {:error, :invalid_argument} 53 | e in Protocol.UndefinedError -> {:error, "#{e.value} is not a list"} 54 | end 55 | end 56 | 57 | assert safe_list_access.([1, 2, 3], 1) == ___ 58 | assert safe_list_access.([1, 2, 3], "a") == ___ 59 | assert safe_list_access.("abc", 0) == ___ 60 | end 61 | 62 | koan "Multiple rescue clauses handle different exceptions" do 63 | risky_operation = fn input -> 64 | try do 65 | case input do 66 | "divide" -> 10 / 0 67 | "access" -> Map.fetch!(%{}, :missing_key) 68 | "convert" -> String.to_integer("not_a_number") 69 | _ -> {:ok, "success"} 70 | end 71 | rescue 72 | ArithmeticError -> {:error, :arithmetic} 73 | KeyError -> {:error, :missing_key} 74 | ArgumentError -> {:error, :invalid_argument} 75 | end 76 | end 77 | 78 | assert risky_operation.("divide") == ___ 79 | assert risky_operation.("access") == ___ 80 | assert risky_operation.("convert") == ___ 81 | assert risky_operation.("safe") == ___ 82 | end 83 | 84 | koan "Try-catch handles thrown values" do 85 | result = 86 | try do 87 | throw(:early_return) 88 | "this won't be reached" 89 | catch 90 | :early_return -> "caught thrown value" 91 | end 92 | 93 | assert result == ___ 94 | end 95 | 96 | koan "After clause always executes for cleanup" do 97 | cleanup_called = 98 | try do 99 | raise "something went wrong" 100 | rescue 101 | RuntimeError -> :returned_value 102 | after 103 | IO.puts("Executed but not returned") 104 | end 105 | 106 | assert cleanup_called == ___ 107 | end 108 | 109 | koan "After executes even when there's no error" do 110 | {result, value} = 111 | try do 112 | {:success, "it worked"} 113 | after 114 | IO.puts("Executed but not returned") 115 | end 116 | 117 | assert result == ___ 118 | assert value == ___ 119 | end 120 | 121 | defmodule CustomError do 122 | defexception message: "something custom went wrong" 123 | end 124 | 125 | koan "Custom exceptions can be defined and raised" do 126 | result = 127 | try do 128 | raise CustomError, message: "custom failure" 129 | rescue 130 | e in CustomError -> "caught custom error: #{e.message}" 131 | end 132 | 133 | assert result == ___ 134 | end 135 | 136 | koan "Bang functions raise exceptions on failure" do 137 | result = 138 | try do 139 | Map.fetch!(%{a: 1}, :b) 140 | rescue 141 | KeyError -> "key not found" 142 | end 143 | 144 | assert result == ___ 145 | end 146 | 147 | koan "Exit signals can be caught and handled" do 148 | result = 149 | try do 150 | exit(:normal) 151 | catch 152 | :exit, :normal -> "caught normal exit" 153 | end 154 | 155 | assert result == ___ 156 | end 157 | 158 | koan "Multiple clauses can handle different error patterns" do 159 | handle_database_operation = fn operation -> 160 | try do 161 | case operation do 162 | :connection_error -> raise "connection failed" 163 | :timeout -> exit(:timeout) 164 | :invalid_query -> throw(:bad_query) 165 | :success -> {:ok, "data retrieved"} 166 | end 167 | rescue 168 | e in RuntimeError -> {:error, {:exception, e.message}} 169 | catch 170 | :exit, :timeout -> {:error, :timeout} 171 | :bad_query -> {:error, :invalid_query} 172 | end 173 | end 174 | 175 | assert handle_database_operation.(:connection_error) == ___ 176 | assert handle_database_operation.(:timeout) == ___ 177 | assert handle_database_operation.(:invalid_query) == ___ 178 | assert handle_database_operation.(:success) == ___ 179 | end 180 | 181 | koan "Error information can be preserved and enriched" do 182 | enriched_error = fn -> 183 | try do 184 | String.to_integer("not a number") 185 | rescue 186 | e in ArgumentError -> 187 | {:error, 188 | %{ 189 | type: :conversion_error, 190 | original: e, 191 | context: "user input processing", 192 | message: "Failed to convert string to integer" 193 | }} 194 | end 195 | end 196 | 197 | {:error, error_info} = enriched_error.() 198 | assert error_info.type == ___ 199 | assert error_info.context == ___ 200 | end 201 | end 202 | -------------------------------------------------------------------------------- /lib/koans/23_pipe_operator.ex: -------------------------------------------------------------------------------- 1 | # credo:disable-for-this-file Credo.Check.Warning.IoInspect 2 | # credo:disable-for-this-file Credo.Check.Refactor.MapJoin 3 | defmodule PipeOperator do 4 | @moduledoc false 5 | use Koans 6 | 7 | @intro "The Pipe Operator - Making data transformation elegant and readable" 8 | 9 | koan "The pipe operator passes the result of one function to the next" do 10 | result = 11 | "hello world" 12 | |> String.upcase() 13 | |> String.split(" ") 14 | |> Enum.join("-") 15 | 16 | assert result == ___ 17 | end 18 | 19 | koan "Without pipes, nested function calls can be hard to read" do 20 | nested_result = Enum.join(String.split(String.downcase("Hello World"), " "), "_") 21 | piped_result = "Hello World" |> String.downcase() |> String.split(" ") |> Enum.join("_") 22 | 23 | assert nested_result == piped_result 24 | assert piped_result == ___ 25 | end 26 | 27 | koan "Pipes pass the result as the first argument to the next function" do 28 | result = 29 | [1, 2, 3, 4, 5] 30 | |> Enum.filter(&(&1 > 2)) 31 | |> Enum.map(&(&1 * 2)) 32 | 33 | assert result == ___ 34 | end 35 | 36 | koan "Additional arguments can be passed to piped functions" do 37 | result = 38 | "hello world" 39 | |> String.split(" ") 40 | |> Enum.join(", ") 41 | 42 | assert result == ___ 43 | end 44 | 45 | koan "Pipes work with anonymous functions too" do 46 | double = fn x -> x * 2 end 47 | add_ten = fn x -> x + 10 end 48 | 49 | result = 50 | 5 51 | |> double.() 52 | |> add_ten.() 53 | 54 | assert result == ___ 55 | end 56 | 57 | koan "You can pipe into function captures" do 58 | result = 59 | [1, 2, 3] 60 | |> Enum.map(&Integer.to_string/1) 61 | |> Enum.join("-") 62 | 63 | assert result == ___ 64 | end 65 | 66 | koan "Complex data transformations become readable with pipes" do 67 | users = [ 68 | %{name: "Bob", age: 25, active: false}, 69 | %{name: "Charlie", age: 35, active: true}, 70 | %{name: "Alice", age: 30, active: true} 71 | ] 72 | 73 | active_names = 74 | users 75 | |> Enum.filter(& &1.active) 76 | |> Enum.map(& &1.name) 77 | |> Enum.sort() 78 | 79 | assert active_names == ___ 80 | end 81 | 82 | koan "Pipes can be split across multiple lines for readability" do 83 | result = 84 | "the quick brown fox jumps over the lazy dog" 85 | |> String.split(" ") 86 | |> Enum.filter(&(String.length(&1) > 3)) 87 | |> Enum.map(&String.upcase/1) 88 | |> Enum.take(3) 89 | 90 | assert result == ___ 91 | end 92 | 93 | 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 94 | result = 95 | [1, 2, 3] 96 | |> Enum.map(&(&1 * 2)) 97 | |> then(&Enum.zip([:a, :b, :c], &1)) 98 | 99 | assert result == ___ 100 | end 101 | 102 | koan "Pipes can be used with case statements" do 103 | process_number = fn x -> 104 | x 105 | |> Integer.parse() 106 | |> case do 107 | {num, ""} -> {:ok, num * 2} 108 | _ -> {:error, :invalid_number} 109 | end 110 | end 111 | 112 | assert process_number.("42") == ___ 113 | assert process_number.("abc") == ___ 114 | end 115 | 116 | koan "Conditional pipes can use if/unless" do 117 | process_string = fn str, should_upcase -> 118 | str 119 | |> String.trim() 120 | |> then(&if should_upcase, do: String.upcase(&1), else: &1) 121 | |> String.split(" ") 122 | end 123 | 124 | assert process_string.(" hello world ", true) == ___ 125 | assert process_string.(" hello world ", false) == ___ 126 | end 127 | 128 | koan "Pipes work great with Enum functions for data processing" do 129 | sales_data = [ 130 | %{product: "Widget", amount: 100, month: "Jan"}, 131 | %{product: "Gadget", amount: 200, month: "Jan"}, 132 | %{product: "Widget", amount: 150, month: "Feb"}, 133 | %{product: "Gadget", amount: 180, month: "Feb"} 134 | ] 135 | 136 | widget_total = 137 | sales_data 138 | |> Enum.filter(&(&1.product == "Widget")) 139 | |> Enum.map(& &1.amount) 140 | |> Enum.sum() 141 | 142 | assert widget_total == ___ 143 | end 144 | 145 | koan "Tap lets you perform side effects without changing the pipeline" do 146 | result = 147 | [1, 2, 3] 148 | |> Enum.map(&(&1 * 2)) 149 | |> tap(&IO.inspect(&1, label: "After doubling")) 150 | |> Enum.sum() 151 | 152 | assert result == ___ 153 | end 154 | 155 | koan "Multiple transformations can be chained elegantly" do 156 | text = "The quick brown fox dumped over the lazy dog" 157 | 158 | word_stats = 159 | text 160 | |> String.downcase() 161 | |> String.split(" ") 162 | |> Enum.group_by(&String.first/1) 163 | |> Enum.map(fn {letter, words} -> {letter, length(words)} end) 164 | |> Enum.into(%{}) 165 | 166 | assert word_stats["d"] == ___ 167 | assert word_stats["t"] == ___ 168 | assert word_stats["q"] == ___ 169 | end 170 | 171 | koan "Pipes can be used in function definitions for clean APIs" do 172 | defmodule TextProcessor do 173 | @moduledoc false 174 | def clean_and_count(text) do 175 | text 176 | |> String.trim() 177 | |> String.downcase() 178 | |> String.replace(~r/[^\w\s]/, "") 179 | |> String.split() 180 | |> length() 181 | end 182 | end 183 | 184 | assert TextProcessor.clean_and_count(" Hello, World! How are you? ") == ___ 185 | end 186 | 187 | koan "Error handling can be integrated into pipelines" do 188 | safe_divide = fn 189 | {x, 0} -> {:error, :division_by_zero} 190 | {x, y} -> {:ok, x / y} 191 | end 192 | 193 | pipeline = fn x, y -> 194 | {x, y} 195 | |> safe_divide.() 196 | |> case do 197 | {:ok, result} -> "Result: #{result}" 198 | {:error, reason} -> "Error: #{reason}" 199 | end 200 | end 201 | 202 | assert pipeline.(10, 2) == ___ 203 | assert pipeline.(10, 0) == ___ 204 | end 205 | end 206 | -------------------------------------------------------------------------------- /lib/koans/24_with_statement.ex: -------------------------------------------------------------------------------- 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 | email: "user@example.com" 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: "test@example.com", 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 188 | -------------------------------------------------------------------------------- /lib/koans/18_genservers.ex: -------------------------------------------------------------------------------- 1 | defmodule GenServers do 2 | @moduledoc false 3 | use Koans 4 | 5 | @intro "GenServers" 6 | 7 | defmodule Laptop do 8 | @moduledoc false 9 | use GenServer 10 | 11 | ##### 12 | # External API 13 | def init(args) do 14 | {:ok, args} 15 | end 16 | 17 | def start(init_password) do 18 | # The __MODULE__ macro returns the current module name as an atom 19 | GenServer.start(__MODULE__, init_password, name: __MODULE__) 20 | end 21 | 22 | def stop do 23 | GenServer.stop(__MODULE__) 24 | end 25 | 26 | def unlock(password) do 27 | GenServer.call(__MODULE__, {:unlock, password}) 28 | end 29 | 30 | def owner_name do 31 | GenServer.call(__MODULE__, :get_owner_name) 32 | end 33 | 34 | def manufacturer do 35 | GenServer.call(__MODULE__, :get_manufacturer) 36 | end 37 | 38 | def laptop_type do 39 | GenServer.call(__MODULE__, :get_type) 40 | end 41 | 42 | def retrieve_password do 43 | GenServer.call(__MODULE__, :get_password) 44 | end 45 | 46 | def laptop_specs do 47 | GenServer.call(__MODULE__, :get_specs) 48 | end 49 | 50 | def change_password(old_password, new_password) do 51 | GenServer.cast(__MODULE__, {:change_password, old_password, new_password}) 52 | end 53 | 54 | #### 55 | # GenServer implementation 56 | 57 | def handle_call(:get_password, _from, current_password) do 58 | {:reply, current_password, current_password} 59 | end 60 | 61 | def handle_call(:get_manufacturer, _from, current_state) do 62 | {:reply, "Apple Inc.", current_state} 63 | end 64 | 65 | def handle_call(:get_type, _from, current_state) do 66 | {:reply, "MacBook Pro", current_state} 67 | end 68 | 69 | def handle_call(:get_owner_name, _from, current_state) do 70 | {:reply, {:ok, "Jack Sparrow"}, current_state} 71 | end 72 | 73 | def handle_call(:get_specs, _from, current_state) do 74 | {:reply, {:ok, ["2.9 GHz Intel Core i5"], 8192, :intel_iris_graphics}, current_state} 75 | end 76 | 77 | def handle_call(:name_check, _from, current_state) do 78 | {:reply, "Congrats! Your process was successfully named.", current_state} 79 | end 80 | 81 | def handle_call({:unlock, password}, _from, current_password) do 82 | case password do 83 | password when password === current_password -> 84 | {:reply, {:ok, "Laptop unlocked!"}, current_password} 85 | 86 | _ -> 87 | {:reply, {:error, "Incorrect password!"}, current_password} 88 | end 89 | end 90 | 91 | def handle_cast({:change_password, old_password, new_password}, current_password) do 92 | case old_password do 93 | old_password when old_password == current_password -> 94 | {:noreply, new_password} 95 | 96 | _ -> 97 | {:noreply, current_password} 98 | end 99 | end 100 | end 101 | 102 | koan "Servers that are created and initialized successfully returns a tuple that holds the PID of the server" do 103 | {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") 104 | assert is_pid(pid) == ___ 105 | end 106 | 107 | koan "The handle_call callback is synchronous so it will block until a reply is received" do 108 | {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") 109 | assert GenServer.call(pid, :get_password) == ___ 110 | end 111 | 112 | koan "A server can support multiple actions by implementing multiple handle_call functions" do 113 | {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") 114 | assert GenServer.call(pid, :get_manufacturer) == ___ 115 | assert GenServer.call(pid, :get_type) == ___ 116 | end 117 | 118 | koan "A handler can return multiple values and of different types" do 119 | {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") 120 | {:ok, processor, memory, graphics} = GenServer.call(pid, :get_specs) 121 | assert processor == ___ 122 | assert memory == ___ 123 | assert graphics == ___ 124 | end 125 | 126 | koan "The handle_cast callback handles asynchronous messages" do 127 | {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") 128 | GenServer.cast(pid, {:change_password, "3kr3t!", "73x7!n9"}) 129 | assert GenServer.call(pid, :get_password) == ___ 130 | end 131 | 132 | koan "Handlers can also return error responses" do 133 | {:ok, pid} = GenServer.start_link(Laptop, "3kr3t!") 134 | assert GenServer.call(pid, {:unlock, "81u3pr!n7"}) == ___ 135 | end 136 | 137 | koan "Referencing processes by their PID gets old pretty quickly, so let's name them" do 138 | {:ok, _} = GenServer.start_link(Laptop, "3kr3t!", name: :macbook) 139 | assert GenServer.call(:macbook, :name_check) == ___ 140 | end 141 | 142 | koan "Our server works but it's pretty ugly to use; so lets use a cleaner interface" do 143 | Laptop.start("EL!73") 144 | assert Laptop.unlock("EL!73") == ___ 145 | end 146 | 147 | koan "Let's use the remaining functions in the external API" do 148 | Laptop.start("EL!73") 149 | 150 | {_, response} = Laptop.unlock("EL!73") 151 | assert response == ___ 152 | 153 | Laptop.change_password("EL!73", "Elixir") 154 | 155 | {_, response} = Laptop.unlock("EL!73") 156 | assert response == ___ 157 | 158 | {_, response} = Laptop.owner_name() 159 | assert response == ___ 160 | 161 | :ok = Laptop.stop() 162 | end 163 | 164 | defmodule TimeoutServer do 165 | @moduledoc false 166 | use GenServer 167 | 168 | def start_link(timeout) do 169 | GenServer.start_link(__MODULE__, timeout, name: __MODULE__) 170 | end 171 | 172 | def init(timeout) do 173 | {:ok, %{count: 0}, timeout} 174 | end 175 | 176 | def get_count do 177 | GenServer.call(__MODULE__, :get_count) 178 | end 179 | 180 | def handle_call(:get_count, _from, state) do 181 | {:reply, state.count, state} 182 | end 183 | 184 | def handle_info(:timeout, state) do 185 | new_state = %{state | count: state.count + 1} 186 | {:noreply, new_state} 187 | end 188 | end 189 | 190 | koan "GenServers can handle info messages and timeouts" do 191 | {:ok, _pid} = TimeoutServer.start_link(100) 192 | # Wait for timeout to occur 193 | :timer.sleep(101) 194 | count = TimeoutServer.get_count() 195 | assert count == ___ 196 | 197 | GenServer.stop(TimeoutServer) 198 | end 199 | 200 | defmodule CrashableServer do 201 | @moduledoc false 202 | use GenServer 203 | 204 | def start_link(initial) do 205 | GenServer.start_link(__MODULE__, initial, name: __MODULE__) 206 | end 207 | 208 | def init(initial) do 209 | {:ok, initial} 210 | end 211 | 212 | def crash do 213 | GenServer.cast(__MODULE__, :crash) 214 | end 215 | 216 | def get_state do 217 | GenServer.call(__MODULE__, :get_state) 218 | end 219 | 220 | def handle_call(:get_state, _from, state) do 221 | {:reply, state, state} 222 | end 223 | 224 | def handle_cast(:crash, _state) do 225 | raise "Intentional crash for testing" 226 | end 227 | end 228 | 229 | koan "GenServers can be supervised and restarted" do 230 | # Start under a supervisor 231 | children = [{CrashableServer, "the state"}] 232 | {:ok, supervisor} = Supervisor.start_link(children, strategy: :one_for_one) 233 | 234 | # Server should be running 235 | initial_state = CrashableServer.get_state() 236 | assert initial_state == ___ 237 | 238 | :ok = CrashableServer.crash() 239 | # Wait for recovery 240 | :timer.sleep(100) 241 | 242 | state_after_crash_recovery = CrashableServer.get_state() 243 | assert state_after_crash_recovery == ___ 244 | 245 | Supervisor.stop(supervisor) 246 | end 247 | end 248 | --------------------------------------------------------------------------------