-
-
Notifications
You must be signed in to change notification settings - Fork 111
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add concept random
and concept exercise maze-maker
#613
Merged
Merged
Changes from all commits
Commits
Show all changes
20 commits
Select commit
Hold shift + click to select a range
cf69d0c
Add concept exercise
jiegillet 526642f
Add concept
jiegillet 8b3da1c
Update exercises/concept/maze-maker/.docs/instructions.md
jiegillet 68727c3
Update exercises/concept/maze-maker/.docs/hints.md
jiegillet 5d8ffd2
Update concepts/random/about.md
jiegillet 233bd6f
Expand on seed in about.md
jiegillet 3ee2e43
Merge branch 'main' into jie-random
jiegillet 38cd786
Add random concept to dnd-character
jiegillet 7024cda
Apply suggestions from code review
jiegillet d240576
formatting
jiegillet d5c96d1
Add contributors to dnd-character
jiegillet 05f102c
Grammar fixes
jiegillet 5fbe72f
More detailed hint
jiegillet cbda11b
More peano
jiegillet 5bc8439
format config file
jiegillet 483eb0e
Merge branch 'main' into jie-random
jiegillet 2b0732d
Expand on the peano example
jiegillet d8774ce
wrap todos in lazy
jiegillet d19fcd7
Add contributors
jiegillet ac599cd
configlet generate
jiegillet File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,10 @@ | ||
{ | ||
"blurb": "Learn how to generate random values in Elm", | ||
"authors": [ | ||
"jiegillet" | ||
], | ||
"contributors": [ | ||
"pwadsworth", | ||
"ceddlyburge" | ||
] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,146 @@ | ||
# Introduction | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I decided to keep the intro and about almost exactly the same, because pretty much everything is needed for solving the exercise. |
||
|
||
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" | ||
} | ||
] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of
Random.andThen identity
should be explained since it is necessary for step 4 of the exercise.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well, I did explain what
Random.andThen
does, and I think it's pretty clear whatidentity
is.But really, I don't want to explain this too much, because I feel it's the only "challenging" part which requires a bit of thought. And it's not really that challenging either since I show it here used in exactly the same way and also I left a hint.
I'll think about it some more :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I find this code pretty confusing as well, I can't say I understand at the moment. I do understand identity, and andThen, so maybe its lazy that I am finding confusing. From the comment showing the output, it looks like
generate 3
will create 3 uniformly distributed natural numbers between 0 and 2, is that right? And if so, what controls the range of the natural numbers produced?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe this example is too strange? I was trying to find one that's simple enough and different enough from the exercise one. Let me know if you have a better idea.
First of all,
lazy
doesn't add anything to the logic, all it does is wrap the function in a lambda so that the compiler doesn't infinitely stickpeano
intopeano
.Then, the type of
Random.uniform (Random.constant Zero) []
isGenerator (Generator Peano)
since it picks from a list of generators. We need to flatten it withandThen : (a -> Generator b) -> Generator a -> Generator b
, wherea
isGenerator Peano
and we wantb
to bePeano
. So the identity function fits the job. (isn't this beautiful? That's one of the reasons I love Elm so much)Lastly, what
peano
does is returnZero
with a 50% chance , and itself plus one the other 50%. So the numbers coming out should be 0 (50% of the time), 1 (25% of the time), 2 (12% of the time), 3 (6% of the time), etc. The numbers are not bounded, because the generator could keep pickingpeano
an arbitrary number of time, but it's more and more unlikely.Maybe I should generate more numbers to get a better feel for the distribution.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah I see, that does make sense, and this explanation is great :)
Maybe add it to the document?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 2b0732d