diff --git a/concepts/random/.meta/config.json b/concepts/random/.meta/config.json new file mode 100644 index 00000000..238db0ab --- /dev/null +++ b/concepts/random/.meta/config.json @@ -0,0 +1,10 @@ +{ + "blurb": "Learn how to generate random values in Elm", + "authors": [ + "jiegillet" + ], + "contributors": [ + "pwadsworth", + "ceddlyburge" + ] +} diff --git a/concepts/random/about.md b/concepts/random/about.md new file mode 100644 index 00000000..356ac186 --- /dev/null +++ b/concepts/random/about.md @@ -0,0 +1,176 @@ +# About + +In a pure functional language like Elm, a function called with the same arguments must always return the same value. +Therefore a function with the type signature `rand : Int` can only be implemented as (`rand = 4`)[xkcd-random], which does not bode well for generating random integers. + +So how do we generate random values in Elm? +We split the problem in two: first, we describe the value that we want to generate with a `Random.Generator a`, then we generate a value. + +The first way to generate a value is to create a `Random.Seed` via `initialSeed : Int -> Seed`. +A `Seed` is an opaque type that contains an integer and knows how to transform that integer in a way that will appear random (called pseudorandom). +Once you have a `Seed`, you can use it to generate a value with a `Generator a`. +A `Generator a` is an opaque type that knows how to use the integer inside of a seed to create a pseudorandom value of type `a` as well as a new seed with an updated integer. + +To generate a value, we can use `step : Generator a -> Seed -> ( a, Seed )`, which returns a value and a new seed that we can use to generate further values. + +Let's use these functions to extract `n` random values out of a generator: + +```elm +generate : Int -> Generator a -> List a +generate n generator = generateValuesFromSeed n generator (Random.initialSeed 42) + +generateValuesFromSeed : Int -> Generator a -> Seed -> List a +generateValuesFromSeed n generator seed = + if n <= 0 then + [] + else + let ( value, nextSeed ) = Random.step generator seed + in value :: generateValuesFromSeed (n - 1) generator nextSeed +``` + +Note that all of these functions are pure, so calling them twice with the same arguments will produce the same values. + +```elm +generate 10 (Random.int 1 6) + --> [4, 5, 3, 3, 5, 1, 2, 4, 6, 6] + +generate 10 (Random.int 1 6) + --> [4, 5, 3, 3, 5, 1, 2, 4, 6, 6] +``` + +The second way to generate a value is by using `Random.generate : (a -> msg) -> Generator a -> Cmd msg`, but that can only be done inside an Elm application. +In that case, the Elm runtime may use `step` as well as outside, non-pure resources to generate seeds, and calling the same function twice will give different results. + +From now on, we will focus on generators. + +The `Random` module provides two basic generators for creating integers and floats within a specific range: + +```elm +generate 5 (Random.int -5 5) + --> [0, 3, -5, 5, 0] + +generate 3 (Random.float 0 5) + --> [1.61803, 3.14159, 2.71828] +``` + +Those values can be combined into tuples, or into lists of values: + +```elm +generate 2 (Random.list 3 (Random.int 0 3)) + --> [[0, 3, 3], [1, 3, 2]] + +generate 2 (Random.pair (Random.int 0 3) (Random.float 10 10.3)) + --> [(0, 10.23412), (2, 10.17094)] +``` + +The (`elm-community/random-extra`)[random-extra] package provides a lot more generators for various data structures: strings, dates, dictionaries, arrays, sets, etc. + +We can create generators that will only return a single value using `Random.constant`: + +```elm +generate 4 (Random.constant "hello") + --> ["hello", "hello", "hello", "hello"] +``` + +We can randomly pick from given elements with equal probability using `Random.uniform`: + +```elm +generate 5 (Random.uniform Red [Green, Blue]) + --> [Red, Blue, Blue, Green, Red] +``` + +`Random.uniform` takes two arguments (`Red` and `[Green, Blue]`) to guarantee that there is at least one value to pick from, since a single list could be empty. + +We can also tweak the probabilities using `Random.weighted`: + +```elm +generate 5 (Random.weighted (Red, 80) [(Green, 15), (Blue, 5)]) + --> [Red, Red, Green, Red, Red] +``` + +The values do not need to add up to 100, they will get renormalized anyway. + +We can reach the inside of a generator with `Random.map`: + +```elm +generate 3 (Random.int 1 6 |> Random.map (\n -> n * 10)) + --> [30, 60, 10] +``` + +We can also use `Random.map2` all the way to `Random.map5` to combine more generators: + +```elm +position = + Random.map3 + (\x y z -> Position x y z) + (Random.float -100 100) + (Random.float -100 100) + (Random.float -100 100) + +generate 1 position + --> [Position 33.788 -98.321 10.0845] +``` + +For more complex uses, we have `Random.andThen : (a -> Generator b) -> Generator a -> Generator b` that can use the value generated by one generator to create another: + +```elm +bool = Random.uniform True [False] + +failHalfOfTheTime : Generator a -> Generator (Maybe a) +failHalfOfTheTime generator = + bool + |> Random.andThen + (\boolResult -> + if boolResult then + Random.map Just generator + + else + Random.constant Nothing + ) + +generate 6 (Random.int 0 1 |> failHalfOfTheTime) + --> [Nothing, Just 1, Just 0, Nothing, Just 1, Nothing] +``` + +It is sometimes useful to define a generator self-recursively. +In those cases, you might need to use `Random.lazy` to keep the compiler from unrolling the generator infinitely. + +```elm +type Peano + = Zero + | Next Peano + + +peano : Generator Peano +peano = + Random.uniform (Random.constant Zero) + [ Random.map Next (Random.lazy (\_ -> peano)) + ] + |> Random.andThen identity + +generate 12 peano + --> [Zero, Next(Next(Zero)), Zero, Next(Zero), Zero, Zero, Next(Zero), Zero, Zero, Next(Zero), Next(Next(Next(Zero)))] +``` + +This example is a little heavy, so let's break it down. + +`Peano` is a type that represents positive integers: `Zero` is 0, `Next(Zero)` is 1, `Next(Next(Zero))` is 2, etc. +We define `peano` to give us a random `Peano` number. + +First of all, note that `Random.lazy` doesn't add anything to the logic, it merely prevents the compiler from writing `peano` into `peano` indefinitely. + +We use `Random.uniform` to pick between zero (with 50% probability) and another `Peano` number plus one (with 50% probability). +However, unlike with the previous example, `Random.uniform` is not picking from values (like `Zero`) but instead from generators (like `Random.constant Zero`) since we want to use `peano` itself. +This means that `Random.uniform` will return a value of type `Generator (Generator Peano)`, which is not want we need. + +To "flatten" the generator, we pipe it into `Random.andThen : (a -> Generator b) -> Generator a -> Generator b`, where we want `b` to be `Peano`. +Since `Generator a` is `Generator (Generator Peano)`, `a` must be `Generator Peano`, and the function `(a -> Generator b)` must be of type `(Generator Peano -> Generator Peano)`. +We don't want to modify anything, so `identity` is the right choice. + +Finally, what kind of numbers will `peano` produce? +We know that it will produce 0 50% of the time, or another iteration of itself plus one 50% of the time. +That means the numbers will be 0 (50% of the time), 1 (25% of the time), 2 (12% of the time), 3 (6% of the time), etc. +`peano` can produce arbitrary large numbers, but with exponentially decreasing probability. + +[xkcd-random]: https://xkcd.com/221/ +[random-extra]: https://package.elm-lang.org/packages/elm-community/random-extra/latest/ diff --git a/concepts/random/introduction.md b/concepts/random/introduction.md new file mode 100644 index 00000000..73e24a86 --- /dev/null +++ b/concepts/random/introduction.md @@ -0,0 +1,146 @@ +# Introduction + +In a pure functional language like Elm, a function called with the same arguments must always return the same value. +Therefore a function with the type signature `rand : Int` can only be implemented as `rand = 4`, which does not bode well for generating random integers. + +So how do we generate random values in Elm? +We split the problem in two: first, we describe the value that we want to generate with a `Random.Generator a`, then we generate a value. + +The first way to generate a value is to create a `Random.Seed` via `initialSeed : Int -> Seed` and then use `step : Generator a -> Seed -> ( a, Seed )`, which returns a value and a new seed. +Note that both of these functions are pure, so calling them twice with the same arguments will produce the same values. + +The second way to generate a value is by using `generate : (a -> msg) -> Generator a -> Cmd msg`, but that can only be done inside an Elm application. +In that case, the Elm runtime may use `step` as well as outside, non-pure resources to generate seeds. + +From now on, we will focus on generators. + +Let us pretend, for the sake of showing examples, that we have defined with `initialSeed` and `step` a function `generate : Int -> Generator a -> List a` that generates a number of values from a generator. + +The `Random` module provides two basic generators for generating integers and floats within a specific range: + +```elm +generate 5 (Random.int -5 5) + --> [0, 3, -5, 5, 0] + +generate 3 (Random.float 0 5) + --> [1.61803, 3.14159, 2.71828] +``` + +Those values can be combined into tuples, or into lists of values: + +```elm +generate 2 (Random.list 3 (Random.int 0 3)) + --> [[0, 3, 3], [1, 3, 2]] + +generate 2 (Random.pair (Random.int 0 3) (Random.float 10 10.3)) + --> [(0, 10.23412), (2, 10.17094)] +``` + +The `elm-community/random-extra` package provides a lot more generators for various data structures: strings, dates, dictionaries, arrays, sets, etc. + +We can create generators that will only return a single value using `Random.constant`: + +```elm +generate 4 (Random.constant "hello") + --> ["hello", "hello", "hello", "hello"] +``` + +We can randomly pick from given elements with equal probability using `Random.uniform`: + +```elm +generate 5 (Random.uniform Red [Green, Blue]) + --> [Red, Blue, Blue, Green, Red] +``` + +`Random.uniform` takes two arguments (`Red` and `[Green, Blue]`) to guarantee that there is at least one value to pick from, since a single list could be empty. + +We can also tweak the probabilities using `Random.weighted`: + +```elm +generate 5 (Random.weighted (Red, 80) [(Green, 15), (Blue, 5)]) + --> [Red, Red, Green, Red, Red] +``` + +The values do not need to add up to 100, they will get renormalized anyway. + +We can reach the inside of a generator with `Random.map`: + +```elm +generate 3 (Random.int 1 6 |> Random.map (\n -> n * 10)) + --> [30, 60, 10] +``` + +We can also use `Random.map2` all the way to `Random.map5` to combine more generators: + +```elm +position = + Random.map3 + (\x y z -> Position x y z) + (Random.float -100 100) + (Random.float -100 100) + (Random.float -100 100) + +generate 1 position + --> [Position 33.788 -98.321 10.0845] +``` + +For more complex uses, we have `Random.andThen : (a -> Generator b) -> Generator a -> Generator b` that can use the value generated by one generator to create another: + +```elm +bool = Random.uniform True [False] + +failHalfOfTheTime : Generator a -> Generator (Maybe a) +failHalfOfTheTime generator = + bool + |> Random.andThen + (\boolResult -> + if boolResult then + Random.map Just generator + + else + Random.constant Nothing + ) + +generate 6 (Random.int 0 1 |> failHalfOfTheTime) + --> [Nothing, Just 1, Just 0, Nothing, Just 1, Nothing] +``` + +It is sometimes useful to define a generator self-recursively. +In those cases, you might need to use `Random.lazy` to keep the compiler from unrolling the generator infinitely. + +```elm +type Peano + = Zero + | Next Peano + + +peano : Generator Peano +peano = + Random.uniform (Random.constant Zero) + [ Random.map Next (Random.lazy (\_ -> peano)) + ] + |> Random.andThen identity + +generate 12 peano + --> [Zero, Next(Next(Zero)), Zero, Next(Zero), Zero, Zero, Next(Zero), Zero, Zero, Next(Zero), Next(Next(Next(Zero)))] +``` + +This example is a little heavy, so let's break it down. + +`Peano` is a type that represents positive integers: `Zero` is 0, `Next(Zero)` is 1, `Next(Next(Zero))` is 2, etc. +We define `peano` to give us a random `Peano` number. + +First of all, note that `Random.lazy` doesn't add anything to the logic, it merely prevents the compiler from writing `peano` into `peano` indefinitely. + +We use `Random.uniform` to pick between zero (with 50% probability) and another `Peano` number plus one (with 50% probability). +However, unlike with the previous example, `Random.uniform` is not picking from values (like `Zero`) but instead from generators (like `Random.constant Zero`) since we want to use `peano` itself. +This means that `Random.uniform` will return a value of type `Generator (Generator Peano)`, which is not want we need. + +To "flatten" the generator, we pipe it into `Random.andThen : (a -> Generator b) -> Generator a -> Generator b`, where we want `b` to be `Peano`. +Since `Generator a` is `Generator (Generator Peano)`, `a` must be `Generator Peano`, and the function `(a -> Generator b)` must be of type `(Generator Peano -> Generator Peano)`. +We don't want to modify anything, so `identity` is the right choice. + +Finally, what kind of numbers will `peano` produce? +We know that it will produce 0 50% of the time, or another iteration of itself plus one 50% of the time. +That means the numbers will be 0 (50% of the time), 1 (25% of the time), 2 (12% of the time), 3 (6% of the time), etc. +`peano` can produce arbitrary large numbers, but with exponentially decreasing probability. diff --git a/concepts/random/links.json b/concepts/random/links.json new file mode 100644 index 00000000..96930a56 --- /dev/null +++ b/concepts/random/links.json @@ -0,0 +1,18 @@ +[ + { + "url": "https://package.elm-lang.org/packages/elm/random/latest/", + "description": "Random module documentation" + }, + { + "url": "https://guide.elm-lang.org/effects/random", + "description": "Elm guide on the Random module" + }, + { + "url": "https://package.elm-lang.org/packages/elm-community/random-extra/latest/", + "description": "random-extra package documentation" + }, + { + "url": "https://elmprogramming.com/commands.html", + "description": "Beginning Elm: Commands" + } +] diff --git a/config.json b/config.json index 4800159d..c204272d 100644 --- a/config.json +++ b/config.json @@ -308,6 +308,21 @@ ], "status": "beta" }, + { + "slug": "maze-maker", + "name": "Maze Maker", + "uuid": "ce133a47-bff5-47ad-b6b5-df60b1627ebc", + "concepts": [ + "random" + ], + "prerequisites": [ + "basics-1", + "basics-2", + "tuples", + "lists", + "custom-types" + ] + }, { "slug": "squeaky-clean", "name": "Squeaky Clean", @@ -1073,8 +1088,11 @@ "slug": "dnd-character", "name": "D&D Character", "uuid": "c22b805f-36db-407c-92d1-39ba0e4fa041", - "practices": [], + "practices": [ + "random" + ], "prerequisites": [ + "random", "lists", "records" ], @@ -1187,6 +1205,11 @@ "uuid": "563a82fc-bb43-4866-a3d2-fd35eee5efa5", "slug": "strings", "name": "Strings" + }, + { + "uuid": "357ffb6e-5a73-49b6-8b69-98ecd3c74c33", + "slug": "random", + "name": "Random" } ], "key_features": [ diff --git a/exercises/concept/maze-maker/.docs/hints.md b/exercises/concept/maze-maker/.docs/hints.md new file mode 100644 index 00000000..d21c34d5 --- /dev/null +++ b/exercises/concept/maze-maker/.docs/hints.md @@ -0,0 +1,46 @@ +# Hints + +## General + +- The documentation for the `Random` package can be found [here][random-module] + +## 1. Start with the end + +- Use the [`Random.constant`][random-constant] function + +## 2. You can't catch flies with vinegar + +- Use the [`Random.uniform`][random-uniform] function for generating the treasures +- Use the [`Random.map`][random-map] function for wrapping the treasure + +## 3. Branching out + +- Use the [`Random.int`][random-int] function for generating a value between 2 and 4 +- You can access this value to create a `Branch` with the [`Random.andThen`][random-andThen] function +- Use the [`Random.list`][random-list] function to generate the branches +- Use the [`Random.map`][random-map] function for wrapping the branches + +## 4. Amazing mazes + +- Use the [`Random.weighted`][random-weighted] function for generating an uneven distribution of outcomes +- Use the [`Random.lazy`][random-lazy] function for using a generator recursively +- Read more about defining recursive data structures [here][bad-recursion] +- If you end up defining a nested `Generator (Generator Maze)` generator, you can use [`Random.andThen`][random-andThen] together with the `identity` function to flatten it + +## 5. We have to go deeper + +- Use the [`Random.uniform`][random-uniform] function for generating the dead ends or rooms at depth `0` +- If you end up defining a nested `Generator (Generator Maze)` generator, you can use [`Random.andThen`][random-andThen] together with the `identity` function to flatten it +- Use the [`Random.lazy`][random-lazy] function for using a generator recursively +- Read more about defining recursive data structures [here][bad-recursion] + +[random-module]: https://package.elm-lang.org/packages/elm/random/latest/ +[random-constant]: https://package.elm-lang.org/packages/elm/random/latest/Random#constant +[random-uniform]: https://package.elm-lang.org/packages/elm/random/latest/Random#uniform +[random-map]: https://package.elm-lang.org/packages/elm/random/latest/Random#map +[random-int]: https://package.elm-lang.org/packages/elm/random/latest/Random#int +[random-andThen]: https://package.elm-lang.org/packages/elm/random/latest/Random#andThen +[random-list]: https://package.elm-lang.org/packages/elm/random/latest/Random#list +[random-weighted]: https://package.elm-lang.org/packages/elm/random/latest/Random#weighted +[random-lazy]: https://package.elm-lang.org/packages/elm/random/latest/Random#lazy +[bad-recursion]: https://github.com/elm/compiler/blob/master/hints/bad-recursion.md diff --git a/exercises/concept/maze-maker/.docs/instructions.md b/exercises/concept/maze-maker/.docs/instructions.md new file mode 100644 index 00000000..f26909ee --- /dev/null +++ b/exercises/concept/maze-maker/.docs/instructions.md @@ -0,0 +1,53 @@ +# Instructions + +You are a maze researcher, working to design a new generation of dungeons and adventuring grounds. + +You are a theoretician, which means you are not concerned with the physical layout of mazes (that is a task for maze engineers), but instead you care about their mathematical complexity: branching factor, maximum depth and such. + +You decide to approach your next project with a random generation approach. + +## 1. Start with the end + +Any good maze is mostly dead ends. + +Define the `deadend` generator so that it always generates the `DeadEnd` variant. + +## 2. You can't catch flies with vinegar + +Let's give adventurers a reason to venture in your mazes. + +Define the `treasure` generator so that it generates either `Gold`, `Diamond` or `Friendship` with equal probability. + +Then, define the `room` generator so that it wraps the values generated by `treasure` in the `Room` variant. + +## 3. Branching out + +And now for the heart and soul of a maze: branches. + +The `branch` generator receives a `Generator Maze` argument, which is a generator able to generate an arbitrary maze. + +Define the `branch` generator so that it generates a `Branch` variant containing several branches leading to mazes generated by its argument. + +The number of branches should be between 2 and 4 (any more than that and the maze engineer would get angry), determined randomly with equal probability. + +## 4. Amazing mazes + +Time to do business. + +Define the `maze` generator so that it generates a dead end 60% of the time, a treasure room 15% of the time and a branch 25% of the time. +The branches should generate new mazes using `maze` recursively. +Make sure to use `deadend`, `room` and `branch` in the generator. + +Each branch contains on average 3 mazes, but only 25% of these will branch out again, which is less than one, so your generated mazes are likely to eventually end. + +## 5. We have to go deeper + +Your `maze` is nice, but it has some issues: + +1. 75% of the generated values don't have any branch at all, hardly real mazes +2. Adventurers might find treasure rooms early in the maze and decide to stop exploring +3. While deep mazes can be generated, they are not very likely: a maze of depth of 3 has an approximate 3% chance of being generated, a maze of depth 10 a 0.2% chance (the depth of a maze is calculated with `MazeMakerSupport.mazeDepth`) + +Define the `mazeOfDepth n` generator so that it generates a maze of depth `n`. +Only the deepest level `0` should contain dead ends or treasure rooms (with equal probability), all other levels should be a branch containing mazes of depth `n - 1` generated recursively with `mazeOfDepth`. +Make sure to use `deadend`, `room` and `branch` in the generator. diff --git a/exercises/concept/maze-maker/.docs/introduction.md b/exercises/concept/maze-maker/.docs/introduction.md new file mode 100644 index 00000000..5d5b1cae --- /dev/null +++ b/exercises/concept/maze-maker/.docs/introduction.md @@ -0,0 +1,148 @@ +# Introduction + +## Random + +In a pure functional language like Elm, a function called with the same arguments must always return the same value. +Therefore a function with the type signature `rand : Int` can only be implemented as `rand = 4`, which does not bode well for generating random integers. + +So how do we generate random values in Elm? +We split the problem in two: first, we describe the value that we want to generate with a `Random.Generator a`, then we generate a value. + +The first way to generate a value is to create a `Random.Seed` via `initialSeed : Int -> Seed` and then use `step : Generator a -> Seed -> ( a, Seed )`, which returns a value and a new seed. +Note that both of these functions are pure, so calling them twice with the same arguments will produce the same values. + +The second way to generate a value is by using `generate : (a -> msg) -> Generator a -> Cmd msg`, but that can only be done inside an Elm application. +In that case, the Elm runtime may use `step` as well as outside, non-pure resources to generate seeds. + +From now on, we will focus on generators. + +Let us pretend, for the sake of showing examples, that we have defined with `initialSeed` and `step` a function `generate : Int -> Generator a -> List a` that generates a number of values from a generator. + +The `Random` module provides two basic generators for generating integers and floats within a specific range: + +```elm +generate 5 (Random.int -5 5) + --> [0, 3, -5, 5, 0] + +generate 3 (Random.float 0 5) + --> [1.61803, 3.14159, 2.71828] +``` + +Those values can be combined into tuples, or into lists of values: + +```elm +generate 2 (Random.list 3 (Random.int 0 3)) + --> [[0, 3, 3], [1, 3, 2]] + +generate 2 (Random.pair (Random.int 0 3) (Random.float 10 10.3)) + --> [(0, 10.23412), (2, 10.17094)] +``` + +The `elm-community/random-extra` package provides a lot more generators for various data structures: strings, dates, dictionaries, arrays, sets, etc. + +We can create generators that will only return a single value using `Random.constant`: + +```elm +generate 4 (Random.constant "hello") + --> ["hello", "hello", "hello", "hello"] +``` + +We can randomly pick from given elements with equal probability using `Random.uniform`: + +```elm +generate 5 (Random.uniform Red [Green, Blue]) + --> [Red, Blue, Blue, Green, Red] +``` + +`Random.uniform` takes two arguments (`Red` and `[Green, Blue]`) to guarantee that there is at least one value to pick from, since a single list could be empty. + +We can also tweak the probabilities using `Random.weighted`: + +```elm +generate 5 (Random.weighted (Red, 80) [(Green, 15), (Blue, 5)]) + --> [Red, Red, Green, Red, Red] +``` + +The values do not need to add up to 100, they will get renormalized anyway. + +We can reach the inside of a generator with `Random.map`: + +```elm +generate 3 (Random.int 1 6 |> Random.map (\n -> n * 10)) + --> [30, 60, 10] +``` + +We can also use `Random.map2` all the way to `Random.map5` to combine more generators: + +```elm +position = + Random.map3 + (\x y z -> Position x y z) + (Random.float -100 100) + (Random.float -100 100) + (Random.float -100 100) + +generate 1 position + --> [Position 33.788 -98.321 10.0845] +``` + +For more complex uses, we have `Random.andThen : (a -> Generator b) -> Generator a -> Generator b` that can use the value generated by one generator to create another: + +```elm +bool = Random.uniform True [False] + +failHalfOfTheTime : Generator a -> Generator (Maybe a) +failHalfOfTheTime generator = + bool + |> Random.andThen + (\boolResult -> + if boolResult then + Random.map Just generator + + else + Random.constant Nothing + ) + +generate 6 (Random.int 0 1 |> failHalfOfTheTime) + --> [Nothing, Just 1, Just 0, Nothing, Just 1, Nothing] +``` + +It is sometimes useful to define a generator self-recursively. +In those cases, you might need to use `Random.lazy` to keep the compiler from unrolling the generator infinitely. + +```elm +type Peano + = Zero + | Next Peano + + +peano : Generator Peano +peano = + Random.uniform (Random.constant Zero) + [ Random.map Next (Random.lazy (\_ -> peano)) + ] + |> Random.andThen identity + +generate 12 peano + --> [Zero, Next(Next(Zero)), Zero, Next(Zero), Zero, Zero, Next(Zero), Zero, Zero, Next(Zero), Next(Next(Next(Zero)))] +``` + +This example is a little heavy, so let's break it down. + +`Peano` is a type that represents positive integers: `Zero` is 0, `Next(Zero)` is 1, `Next(Next(Zero))` is 2, etc. +We define `peano` to give us a random `Peano` number. + +First of all, note that `Random.lazy` doesn't add anything to the logic, it merely prevents the compiler from writing `peano` into `peano` indefinitely. + +We use `Random.uniform` to pick between zero (with 50% probability) and another `Peano` number plus one (with 50% probability). +However, unlike with the previous example, `Random.uniform` is not picking from values (like `Zero`) but instead from generators (like `Random.constant Zero`) since we want to use `peano` itself. +This means that `Random.uniform` will return a value of type `Generator (Generator Peano)`, which is not want we need. + +To "flatten" the generator, we pipe it into `Random.andThen : (a -> Generator b) -> Generator a -> Generator b`, where we want `b` to be `Peano`. +Since `Generator a` is `Generator (Generator Peano)`, `a` must be `Generator Peano`, and the function `(a -> Generator b)` must be of type `(Generator Peano -> Generator Peano)`. +We don't want to modify anything, so `identity` is the right choice. + +Finally, what kind of numbers will `peano` produce? +We know that it will produce 0 50% of the time, or another iteration of itself plus one 50% of the time. +That means the numbers will be 0 (50% of the time), 1 (25% of the time), 2 (12% of the time), 3 (6% of the time), etc. +`peano` can produce arbitrary large numbers, but with exponentially decreasing probability. diff --git a/exercises/concept/maze-maker/.docs/introduction.md.tpl b/exercises/concept/maze-maker/.docs/introduction.md.tpl new file mode 100644 index 00000000..d3d95c63 --- /dev/null +++ b/exercises/concept/maze-maker/.docs/introduction.md.tpl @@ -0,0 +1,3 @@ +# Introduction + +%{concept: random} diff --git a/exercises/concept/maze-maker/.meta/Exemplar.elm b/exercises/concept/maze-maker/.meta/Exemplar.elm new file mode 100644 index 00000000..84ddcf18 --- /dev/null +++ b/exercises/concept/maze-maker/.meta/Exemplar.elm @@ -0,0 +1,56 @@ +module MazeMaker exposing (..) + +import Random exposing (Generator) + + +type Maze + = DeadEnd + | Room Treasure + | Branch (List Maze) + + +type Treasure + = Gold + | Diamond + | Friendship + + +deadend : Generator Maze +deadend = + Random.constant DeadEnd + + +treasure : Generator Treasure +treasure = + Random.uniform Friendship [ Gold, Diamond ] + + +room : Generator Maze +room = + Random.map Room treasure + + +branch : Generator Maze -> Generator Maze +branch mazeGenerator = + Random.int 2 4 + |> Random.andThen + (\n -> Random.list n mazeGenerator |> Random.map Branch) + + +maze : Generator Maze +maze = + Random.weighted + ( 0.6, deadend ) + [ ( 0.15, room ) + , ( 0.25, branch (Random.lazy (\_ -> maze)) ) + ] + |> Random.andThen identity + + +mazeOfDepth : Int -> Generator Maze +mazeOfDepth depth = + if depth == 0 then + Random.uniform deadend [ room ] |> Random.andThen identity + + else + branch (Random.lazy (\_ -> mazeOfDepth (depth - 1))) diff --git a/exercises/concept/maze-maker/.meta/config.json b/exercises/concept/maze-maker/.meta/config.json new file mode 100644 index 00000000..c6cb6ac2 --- /dev/null +++ b/exercises/concept/maze-maker/.meta/config.json @@ -0,0 +1,24 @@ +{ + "authors": [ + "jiegillet" + ], + "contributors": [ + "pwadsworth", + "ceddlyburge" + ], + "files": { + "solution": [ + "src/MazeMaker.elm" + ], + "test": [ + "tests/Tests.elm" + ], + "exemplar": [ + ".meta/Exemplar.elm" + ], + "editor": [ + "src/MazeMakerSupport.elm" + ] + }, + "blurb": "Learn to generate random values in Elm" +} diff --git a/exercises/concept/maze-maker/.meta/design.md b/exercises/concept/maze-maker/.meta/design.md new file mode 100644 index 00000000..9db6de9e --- /dev/null +++ b/exercises/concept/maze-maker/.meta/design.md @@ -0,0 +1,48 @@ +# Design + +## Goal + +The students will learn to generate random values through the use of generators. + +## Learning objectives + +Students will be able to + +- Explain the general idea of a `Generator` +- Explain why a pure language can't use `math.random()` +- Use primitive generators: `int`, `float`, `uniform`, `weighted`, `constant` +- Combine them with data structures: `pair`, `list` +- Modify generators with `map`, `mapN`, `andThen` +- Know to use `lazy` for recursive applications +- Look up more helpers in `elm-community/random-extra` + +## Out of scope + +- Using `generate` (we can only mention because it returns a `Cmd msg`) +- Generate values: `Seed`, `step`, `initialSeed` (only mentioned, not used) +- `independentSeed` +- `maxInt`, `minInt` + +## Concepts + +The concepts this exercise unlock are: + +- random + +## Prerequisites + +- basics-1 +- basics-2 +- tuples +- lists +- custom-types + +## Analyzer + +Make sure that: + +- `room` uses `treasure` +- `maze` uses `deadend`, `branch` and `room` +- `maze` uses `maze` recursively +- `mazeOfDepth` uses `deadend`, `branch` and `room` +- `mazeOfDepth` uses `mazeOfDepth` recursively diff --git a/exercises/concept/maze-maker/elm.json b/exercises/concept/maze-maker/elm.json new file mode 100644 index 00000000..22c9e137 --- /dev/null +++ b/exercises/concept/maze-maker/elm.json @@ -0,0 +1,29 @@ +{ + "type": "application", + "source-directories": [ + "src" + ], + "elm-version": "0.19.1", + "dependencies": { + "direct": { + "elm/core": "1.0.5", + "elm/json": "1.1.3", + "elm/parser": "1.1.0", + "elm/random": "1.0.0", + "elm/regex": "1.0.0", + "elm/time": "1.0.0" + }, + "indirect": {} + }, + "test-dependencies": { + "direct": { + "elm-explorations/test": "2.1.0", + "rtfeldman/elm-iso8601-date-strings": "1.1.4" + }, + "indirect": { + "elm/bytes": "1.0.8", + "elm/html": "1.0.0", + "elm/virtual-dom": "1.0.3" + } + } +} diff --git a/exercises/concept/maze-maker/src/MazeMaker.elm b/exercises/concept/maze-maker/src/MazeMaker.elm new file mode 100644 index 00000000..7c0a635c --- /dev/null +++ b/exercises/concept/maze-maker/src/MazeMaker.elm @@ -0,0 +1,45 @@ +module MazeMaker exposing (..) + +import Random exposing (Generator) + + +type Maze + = DeadEnd + | Room Treasure + | Branch (List Maze) + + +type Treasure + = Gold + | Diamond + | Friendship + + +deadend : Generator Maze +deadend = + Random.lazy (\_ -> Debug.todo "Please implement deadend") + + +treasure : Generator Treasure +treasure = + Random.lazy (\_ -> Debug.todo "Please implement treasure") + + +room : Generator Maze +room = + Random.lazy (\_ -> Debug.todo "Please implement room") + + +branch : Generator Maze -> Generator Maze +branch mazeGenerator = + Random.lazy (\_ -> Debug.todo "Please implement branch") + + +maze : Generator Maze +maze = + Random.lazy (\_ -> Debug.todo "Please implement maze") + + +mazeOfDepth : Int -> Generator Maze +mazeOfDepth depth = + Random.lazy (\_ -> Debug.todo "Please implement mazeOfDepth") diff --git a/exercises/concept/maze-maker/src/MazeMakerSupport.elm b/exercises/concept/maze-maker/src/MazeMakerSupport.elm new file mode 100644 index 00000000..5e9e51cb --- /dev/null +++ b/exercises/concept/maze-maker/src/MazeMakerSupport.elm @@ -0,0 +1,62 @@ +module MazeMakerSupport exposing (..) + +import MazeMaker exposing (Maze(..)) + + +isBranch : Maze -> Bool +isBranch maze = + case maze of + DeadEnd -> + False + + Room _ -> + False + + Branch _ -> + True + + +isRoom : Maze -> Bool +isRoom maze = + case maze of + DeadEnd -> + False + + Room _ -> + True + + Branch _ -> + False + + +branchNumber : Maze -> Int +branchNumber maze = + case maze of + DeadEnd -> + 0 + + Room _ -> + 0 + + Branch mazes -> + mazes + |> List.map branchNumber + |> List.sum + |> (+) (List.length mazes) + + +mazeDepth : Maze -> Int +mazeDepth maze = + case maze of + DeadEnd -> + 0 + + Room _ -> + 0 + + Branch mazes -> + mazes + |> List.map mazeDepth + |> List.maximum + |> Maybe.withDefault 0 + |> (+) 1 diff --git a/exercises/concept/maze-maker/tests/Tests.elm b/exercises/concept/maze-maker/tests/Tests.elm new file mode 100644 index 00000000..48b1221f --- /dev/null +++ b/exercises/concept/maze-maker/tests/Tests.elm @@ -0,0 +1,187 @@ +module Tests exposing (tests) + +import Expect +import Fuzz +import MazeMaker exposing (Maze(..), Treasure(..), branch) +import MazeMakerSupport +import Parser exposing (Step(..)) +import Random +import Test exposing (Test, describe, fuzz, fuzzWith) +import Test.Distribution + + +tests : Test +tests = + describe "MazeMaker" + [ describe "1" + [ fuzz (Fuzz.fromGenerator MazeMaker.deadend) + "deadend always generates a Deadend" + <| + \deadend -> deadend |> Expect.equal DeadEnd + ] + , describe "2" + [ fuzzWith + { runs = 100 + , distribution = + Test.expectDistribution + [ ( Test.Distribution.atLeast 30, "is Friendship", (==) Friendship ) + , ( Test.Distribution.atLeast 30, "is Gold", (==) Gold ) + , ( Test.Distribution.atLeast 30, "is Diamond", (==) Diamond ) + ] + } + (Fuzz.fromGenerator MazeMaker.treasure) + "treasure generates all three kinds of Treasure equally" + <| + \_ -> Expect.pass + , fuzzWith + { runs = 100 + , distribution = + Test.expectDistribution + [ ( Test.Distribution.atLeast 30, "is Room Friendship", (==) (Room Friendship) ) + , ( Test.Distribution.atLeast 30, "is Room Gold", (==) (Room Gold) ) + , ( Test.Distribution.atLeast 30, "is Room Diamond", (==) (Room Diamond) ) + ] + } + (Fuzz.fromGenerator MazeMaker.room) + "room generates a Room with all three kinds of Treasure equally represented" + <| + \_ -> Expect.pass + ] + , describe "3" + [ fuzz + (Fuzz.fromGenerator (MazeMaker.branch MazeMaker.deadend)) + "branch generates a Branch variant" + <| + \branch -> MazeMakerSupport.isBranch branch |> Expect.equal True + , fuzz + (Fuzz.fromGenerator (MazeMaker.branch MazeMaker.deadend)) + "branch generates branches with deadends" + <| + \branch -> + case branch of + Branch branches -> + List.all ((==) DeadEnd) branches |> Expect.equal True + + _ -> + Expect.fail "should only generate a Branch" + , fuzz + (Fuzz.fromGenerator (MazeMaker.branch MazeMaker.room)) + "branch generates branches with rooms" + <| + \branch -> + case branch of + Branch branches -> + List.all MazeMakerSupport.isRoom branches |> Expect.equal True + + _ -> + Expect.fail "should only generate a Branch" + , fuzz + (Fuzz.fromGenerator (MazeMaker.branch MazeMaker.deadend)) + "branch generates at least two branches" + <| + \branch -> MazeMakerSupport.branchNumber branch |> Expect.atLeast 2 + , fuzz + (Fuzz.fromGenerator (MazeMaker.branch MazeMaker.deadend)) + "branch generates at most four branches" + <| + \branch -> MazeMakerSupport.branchNumber branch |> Expect.atMost 4 + , fuzzWith + { runs = 100 + , distribution = + Test.expectDistribution + [ ( Test.Distribution.atLeast 30, "has 2 branches", (==) 2 ) + , ( Test.Distribution.atLeast 30, "has 3 branches", (==) 3 ) + , ( Test.Distribution.atLeast 30, "has 4 branches", (==) 4 ) + ] + } + (Fuzz.fromGenerator (MazeMaker.branch MazeMaker.deadend |> Random.map MazeMakerSupport.branchNumber)) + "branch generates 2, 3, or 4 branches equally" + <| + \_ -> Expect.pass + ] + , describe "4" + [ fuzzWith + { runs = 100 + , distribution = + Test.expectDistribution + [ ( Test.Distribution.atLeast 50, "is a deadend", (==) DeadEnd ) + , ( Test.Distribution.atLeast 10, "is a room", MazeMakerSupport.isRoom ) + , ( Test.Distribution.atLeast 20, "is a branch", MazeMakerSupport.isBranch ) + ] + } + (Fuzz.fromGenerator MazeMaker.maze) + "generates 60% or deadends, 15% of rooms and 25% of branches" + <| + \_ -> Expect.pass + , fuzzWith + { runs = 1000 + , distribution = + Test.expectDistribution + [ ( Test.Distribution.moreThanZero, "depth 0", \m -> MazeMakerSupport.mazeDepth m == 0 ) + , ( Test.Distribution.moreThanZero, "depth 1", \m -> MazeMakerSupport.mazeDepth m == 1 ) + , ( Test.Distribution.moreThanZero, "depth 2", \m -> MazeMakerSupport.mazeDepth m == 2 ) + , ( Test.Distribution.moreThanZero, "depth 3", \m -> MazeMakerSupport.mazeDepth m == 3 ) + ] + } + (Fuzz.fromGenerator MazeMaker.maze) + "can generates mazes of depth at least 3" + <| + \_ -> Expect.pass + ] + , describe "5" + [ fuzz (Fuzz.fromGenerator (MazeMaker.mazeOfDepth 0)) + "mazeOfDepth 0 generates a maze of depth 0" + <| + \maze -> MazeMakerSupport.mazeDepth maze |> Expect.equal 0 + , fuzz (Fuzz.fromGenerator (MazeMaker.mazeOfDepth 1)) + "mazeOfDepth 1 generates a maze of depth 1" + <| + \maze -> MazeMakerSupport.mazeDepth maze |> Expect.equal 1 + , fuzz (Fuzz.fromGenerator (MazeMaker.mazeOfDepth 5)) + "mazeOfDepth 5 generates a maze of depth 5" + <| + \maze -> MazeMakerSupport.mazeDepth maze |> Expect.equal 5 + , fuzz (Fuzz.fromGenerator (MazeMaker.mazeOfDepth 1)) + "mazeOfDepth 1 generates a maze with 2 to 4 rooms or deadends" + <| + \maze -> + case maze of + Branch branches -> + if List.all (MazeMakerSupport.isBranch >> not) branches then + List.length branches + |> Expect.all [ Expect.atLeast 2, Expect.atMost 4 ] + + else + Expect.fail "There should only be deadlines and rooms on depth 1 of 2" + + _ -> + Expect.fail "There should only be branches on depth 0 of 1" + , fuzz (Fuzz.fromGenerator (MazeMaker.mazeOfDepth 2)) + "mazeOfDepth 2 generates a maze with rooms and deadends at the bottom exclusively" + <| + \maze -> + let + branchContainsNoBranches branch = + case branch of + Branch bottomBranches -> + List.all (MazeMakerSupport.isBranch >> not) bottomBranches + + _ -> + False + in + case maze of + Branch branches -> + if List.all MazeMakerSupport.isBranch branches then + if List.all branchContainsNoBranches branches then + Expect.pass + + else + Expect.fail "There should only be deadends and rooms on depth 2 of 2" + + else + Expect.fail "There should only be branches on depth 1 of 2" + + _ -> + Expect.fail "There should only be branches on depth 0 of 2" + ] + ] diff --git a/exercises/concept/squeaky-clean/.docs/introduction.md b/exercises/concept/squeaky-clean/.docs/introduction.md index f64eeffa..a26d70ec 100644 --- a/exercises/concept/squeaky-clean/.docs/introduction.md +++ b/exercises/concept/squeaky-clean/.docs/introduction.md @@ -1,15 +1,17 @@ # Introduction +## Strings + Characters and Strings form the basis for text representation in Elm. Strings can be thought of as lists of characters and most `List` functions have their equivalent in the `String` module. -## Characters +### Characters The `Char` type represents a single unicode character with single quotes e.g. `'a'`. The `Char` module provides predicate functions (`Char -> Bool`) to test qualities of a character: `isUpper`, `isLower`, `isAlpha`, and `isAlphaNum` The module also provides functions to convert a character `toUpper`, `toLower`, `toLocaleUpper`, `toLocaleLower`, as well as to/from their numerical unicode value (`Int`) with `toCode` and `fromCode`. -## Strings +### Strings The `String` type provides a built-in representation for efficient string manipulation and can represent any sequence of unicode characters e.g. "this is an Elm string." Multi-line strings are represented with triple double quotes and can have unescaped quotes and newlines. @@ -21,7 +23,7 @@ are preserved. """ ``` -## String Functions +### String Functions The `String` module contains all the functions summarized on the table below. Some of the most commonly used functions include: @@ -80,7 +82,7 @@ map (\c -> if c == '/' then '.' else c) "a/b/c" --> "a.b.c" ``` -## All String Functions: +### All String Functions: | Manipulate | Substrings | Check | Convert | Higher Order | |------------|------------|-------------|--------------|--------------| | `reverse` | `slice` | `length` | `toInt` | `map` | @@ -93,4 +95,4 @@ map (\c -> if c == '/' then '.' else c) "a/b/c" | `words` | | | | | | `lines` | | | | | | `cons` | | | | | -| `uncons` | | | | | \ No newline at end of file +| `uncons` | | | | | diff --git a/exercises/practice/dnd-character/.meta/config.json b/exercises/practice/dnd-character/.meta/config.json index 8efc597f..0f3ed6fd 100644 --- a/exercises/practice/dnd-character/.meta/config.json +++ b/exercises/practice/dnd-character/.meta/config.json @@ -2,6 +2,10 @@ "authors": [ "jiegillet" ], + "contributors": [ + "ceddlyburge", + "mpizenberg" + ], "files": { "solution": [ "src/DndCharacter.elm"