Skip to content
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 20 commits into from
Oct 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions concepts/random/.meta/config.json
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"
]
}
176 changes: 176 additions & 0 deletions concepts/random/about.md
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
Copy link
Contributor

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.

Copy link
Contributor Author

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 what identity 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 :)

Copy link
Contributor

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?

Copy link
Contributor Author

@jiegillet jiegillet Oct 20, 2023

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, lazydoesn't add anything to the logic, all it does is wrap the function in a lambda so that the compiler doesn't infinitely stick peano into peano.

Then, the type of Random.uniform (Random.constant Zero) [] is Generator (Generator Peano) since it picks from a list of generators. We need to flatten it with andThen : (a -> Generator b) -> Generator a -> Generator b, where a is Generator Peano and we want b to be Peano. 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 return Zero 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 picking peano 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.

Copy link
Contributor

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?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 2b0732d


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/
146 changes: 146 additions & 0 deletions concepts/random/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Introduction
Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
The only change I did is remove all the links, because they are considered distracting.


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.
18 changes: 18 additions & 0 deletions concepts/random/links.json
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"
}
]
Loading