Skip to content

Commit

Permalink
Improving on the first draft of the Leap approaches (#1410)
Browse files Browse the repository at this point in the history
* fixing link to the old approach/cond

* removing trailing white spaces from text

* two more white spaces

* fixing typos
  • Loading branch information
michalporeba authored Jan 14, 2024
1 parent 841d716 commit c1694a5
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 66 deletions.
28 changes: 14 additions & 14 deletions exercises/practice/leap/.approaches/clauses/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,41 +10,41 @@ defmodule Year do
end
```

In Elixir, functions can have multiple clauses.
Which one will be executed depends on parameter matching and guards.
When a function with multiple clauses is invoked, the parameters are compared to the definitions in the order in which they were defined, and only the first one matching will be invoked.
In Elixir, functions can have multiple clauses.
Which one will be executed depends on parameter matching and guards.
When a function with multiple clauses is invoked, the parameters are compared to the definitions in the order in which they were defined, and only the first one matching will be invoked.

While in the [operators approach][operators-approach], it was possible to reorder expressions as long as the suitable boolean operators were used, in this approach, there is only one correct order of definitions.
While in the [operators approach][operators-approach], it was possible to reorder expressions as long as the suitable boolean operators were used, in this approach, there is only one correct order of definitions.

In our case, the three guards in the function clauses are as follows:
In our case, the three guards in the function clauses are as follows:

```elixir
when rem(year, 400) == 0
when rem(year, 100) == 0
when rem(year, 100) == 0
when rem(year, 4) == 0
```

But because of the order they are evaluated in, they are equivalent to:

```elixir
when rem(year, 400) == 0
when rem(year, 100) == 0 and not rem(year, 400) == 0
when rem(year, 100) == 0 and not rem(year, 400) == 0
when rem(year, 4) == 0 and not rem(year, 100) == 0 and not rem(year, 400) == 0
```

The final clause, `def leap_year?(_), do: false`, returns false if previous clauses are not a match.

## Guards

The [guards][hexdocs-guards] are part of the pattern-matching mechanism.
They allow for more complex checks of values.
The [guards][hexdocs-guards] are part of the pattern-matching mechanism.
They allow for more complex checks of values.
However, because of when they are executed to allow the compiler to perform necessary optimization,
only a minimal subset of operations is permitted.
`Kernel.rem/2` is on this limited list, and `Integer.mod/2` is not.
This is why, in this approach, only the first one will work, and the latter will not.
only a minimal subset of operations is permitted.
`Kernel.rem/2` is on this limited list, and `Integer.mod/2` is not.
This is why, in this approach, only the first one will work, and the latter will not.

In this approach, the boolean operators matter too. Only the strict ones, `not`, `and`, `or` are allowed.
The relaxed `!`, `&&`, `||` will fail to compile.
In this approach, the boolean operators matter too. Only the strict ones, `not`, `and`, `or` are allowed.
The relaxed `!`, `&&`, `||` will fail to compile.

[operators-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/operators
[hexdocs-guards]: https://hexdocs.pm/elixir/main/patterns-and-guards.html#guards
37 changes: 19 additions & 18 deletions exercises/practice/leap/.approaches/flow/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ defmodule Year do
def leap_year?(year) do
if rem(year, 100) == 0 do
rem(year, 400) == 0
else
else
rem(year, 4) == 0
end
end
Expand All @@ -15,25 +15,26 @@ end

## If

Elixir provides four [control flow structures][hexdocs-structures]: `case`, `cond`, `if`, and `unless`.
The `if` and `unless` allow to evaluate only one condition.
Unlike in many other languages, there is no `else if` option in Elixir.
Elixir provides four [control flow structures][hexdocs-structures]: `case`, `cond`, `if`, and `unless`.

However, in this case, it is not necessary. We can use `if` once to check if the year is divisible by 100.
If it is, then whether it is a leap year or not depends on if it is divisible by 400.
If it is not, then whether it is a leap year or not depends on if it is divisible by 4.
The `if` and `unless` allow to evaluate only one condition.
Unlike in many other languages, there is no `else if` option in Elixir.

However, in this case, it is not necessary. We can use `if` once to check if the year is divisible by 100.
If it is, then whether it is a leap year or not depends on if it is divisible by 400.
If it is not, then whether it is a leap year or not depends on if it is divisible by 4.

```elixir
def leap_year?(year) do
if rem(year, 100) == 0 do
rem(year, 400) == 0
else
else
rem(year, 4) == 0
end
end
```

## Cond
## Cond

Another option is `cond` which allows for evaluating multiple conditions, similar to `else if` in other languages.

Expand All @@ -48,12 +49,12 @@ def leap_year?(year) do
end
```

Similarly to the [multiple clause function approach][clause-approach], the order here matters.
The conditions are evaluated in order, and the first that is not `nil` or `false` leads to the result.
Similarly to the [multiple clause function approach][clause-approach], the order here matters.
The conditions are evaluated in order, and the first that is not `nil` or `false` leads to the result.

## Case

`case` allows to compare a value to multiple patterns, but can also replicate what `if` offers.
`case` allows to compare a value to multiple patterns, but can also replicate what `if` offers.

```elixir
def leap_year?(year) do
Expand All @@ -64,7 +65,7 @@ def leap_year?(year) do
end
```

It also supports [guards][hexdocs-guards], offering another way to solve the problem.
It also supports [guards][hexdocs-guards], offering another way to solve the problem.

```elixir
def leap_year?(year) do
Expand All @@ -74,13 +75,13 @@ def leap_year?(year) do
_ when rem(year, 4) == 0 -> true
_ -> false
end
end
end
```

The `case` can be very flexible, so many variations are possible.
Using it with pattern matching on a tuple is considered **the most idiomatic**.
In this case, a tuple is created with all the checks.
Then, pattern matching to tuples is performed.
The `case` can be very flexible, so many variations are possible.
Using it with pattern matching on a tuple is considered **the most idiomatic**.
In this case, a tuple is created with all the checks.
Then, pattern matching to tuples is performed.

```elixir
def leap_year?(year) do
Expand Down
28 changes: 14 additions & 14 deletions exercises/practice/leap/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,32 @@
# Introduction

Every fourth year is a leap year (with some exceptions), but let's consider this one condition first.
Every fourth year is a leap year (with some exceptions), but let's consider this one condition first.

To solve the Leap problem, we must determine if a year is evenly divisible by a number or if a reminder of an integer division is zero.
Such operation in computing is called [modulo][modulo].
To solve the Leap problem, we must determine if a year is evenly divisible by a number or if a reminder of an integer division is zero.
Such operation in computing is called [modulo][modulo].

Unlike many languages, Elixir does not have [operators][operators] for either integer division or modulo.
Unlike many languages, Elixir does not have [operators][operators] for either integer division or modulo.
Instead, it provides the [`Kernel.rem/2`][rem] and the [`Integer.mod/2`][mod] functions.

The two functions differ in how they work with negative divisors, but since, in this exercise,
all the divisors are non-negative, both could work, depending on the approach you choose.

## General solution

To check if a year is divisible by `n`, we can do `rem(year, n) == 0`.
To check if a year is divisible by `n`, we can do `rem(year, n) == 0`.

Any approach to the problem will perform this check three times to see if a year is equally divisible by 4, 100 and 400.
What will differ between approaches is what Elixir features we will use to combine the checks.
What will differ between approaches is what Elixir features we will use to combine the checks.

## Approach: Boolean operators

The full rules are as follows:
A year is a leap year if
* it is divisible by 4
A year is a leap year if
* it is divisible by 4
* but not divisible by 100
* unless it is divisible by 400

We can use [boolean operators][boolean-operators] to combine the checks, for example, like so:
We can use [boolean operators][boolean-operators] to combine the checks, for example, like so:

```elixir
rem(year, 5) == 0 and not rem(year, 100) == 0 or rem(year, 400) == 0
Expand All @@ -36,7 +36,7 @@ It includes variations of the operators and their precedence.

## Approach: multiple clause function

Instead of using boolean operators, we can define multiple `leap_year?/1` function clauses with different guards.
Instead of using boolean operators, we can define multiple `leap_year?/1` function clauses with different guards.

```elixir
def leap_year?(year) when rem(year, 400) == 0, do: true
Expand All @@ -45,17 +45,17 @@ def leap_year?(year) when rem(year, 4) == 0, do: true
def leap_year?(_), do: false
```

In the [multiple clause function approach][clause-approach] we discuss why in this approach the `Integer.mod/2` function will not work.
In the [multiple clause function approach][clause-approach] we discuss why in this approach the `Integer.mod/2` function will not work.

## Approach: control flow structures

In addition to the above two approaches, control flow structures offer a number of solutions.
Here are two examples using `if` and `case`.
Here are two examples using `if` and `case`.

```elixir
if rem(year, 100) == 0 do
rem(year, 400) == 0
else
else
rem(year, 4) == 0
end
```
Expand All @@ -78,6 +78,6 @@ We discuss these and other solutions depending on various control flow structure
[boolean-operators]: https://hexdocs.pm/elixir/operators.html#general-operators
[operators-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/operators
[clause-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/clauses
[flow-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/cond
[flow-approach]: https://exercism.org/tracks/elixir/exercises/leap/approaches/flow


40 changes: 20 additions & 20 deletions exercises/practice/leap/.approaches/operators/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,40 +11,40 @@ end

## Short-circuiting

At the core of this approach, three checks are returning three boolean values.
At the core of this approach, three checks are returning three boolean values.
We can use [Boolean logic](https://en.wikipedia.org/wiki/Boolean_algebra) to combine the results.

When using this approach, it is essential to consider short-circuiting of boolean operators.
The expression `left and right` can be only true if both `left` and `right` are *true*.
If `left` is *false*, `right` will not be evaluated. The result will be *false*.
When using this approach, it is essential to consider short-circuiting of boolean operators.
The expression `left and right` can be only true if both `left` and `right` are *true*.
If `left` is *false*, `right` will not be evaluated. The result will be *false*.
However, if `left` is *true*, `right` has to be evaluated to determin the outcome.

The expression `left or right` can be true if either `left` or `right` is *true*.
If `left` is *true*, `right` will not be evaluated. The result will be *true*.
The expression `left or right` can be true if either `left` or `right` is *true*.
If `left` is *true*, `right` will not be evaluated. The result will be *true*.
However, if `left` is *false*, `right` has to be evaluated to determine the outcome.

## Precedence of operators

Another thing to consider when using Boolean operators is their precedence.
Another thing to consider when using Boolean operators is their precedence.
```elixir
true or false and false
```
The above evaluates to *true* because in Elixir `and` has higher precedence than `or`.
The above evaluates to *true* because in Elixir `and` has higher precedence than `or`.
The above expression is equivalent to:
```elixir
true or (false and false)
```
If `or` should be evaluated first, we must use parenthesis.
If `or` should be evaluated first, we must use parenthesis.
```elixir
(true or false) and false
```
which equals to *false*.
which equals to *false*.

The `not` operator is evaluated before `and` and `or`.
The `not` operator is evaluated before `and` and `or`.

## Strict or relaxed?

Elixir offers two sets of Boolean operators: strict and relaxed.
Elixir offers two sets of Boolean operators: strict and relaxed.
The strict versions `not`, `and`, `or` require the first (left) argument to be of [boolean type][hexdocs-booleans].
The relaxed versions `!`, `&&`, `||` require the first argument to be only [truthy or falsy][hexdocs-truthy].

Expand All @@ -64,8 +64,8 @@ def leap_year?(year) do
end
```

Some prefer this form, as it is very direct. We can see what is happening.
We are explicitly checking the reminder, comparing it to zero.
Some prefer this form, as it is very direct. We can see what is happening.
We are explicitly checking the reminder, comparing it to zero.

```elixir
defp divides?(number, divisor), do: rem(number, divisor) == 0
Expand All @@ -75,12 +75,12 @@ def leap_year?(year) do
end
```

Other might prefer the above form, which requires defining the `devides?` function or something similar.
By doing so, we can be explicit about the *intent*.
We want to check if a year can be equally divided into a number.
Others might prefer the above form, which requires defining the `devides?` function or something similar.
By doing so, we can be explicit about the *intent*.
We want to check if a year can be equally divided into a number.

Yet another approach might be to use variables to capture the results of individual checks and provided the extra meaning.
This approach also shortens the check so the Boolean operators and relationships between them are more prominent.
Yet another approach might be to use variables to capture the results of individual checks and provide the extra meaning.
This approach also shortens the check so the Boolean operators and relationships between them are more prominent.

```elixir
def leap_year?(year) do
Expand All @@ -91,7 +91,7 @@ def leap_year?(year) do
end
```

All versions of the code will work. Which one to choose is often a personal or sometimes a team preference. What reads best for you? What will make most sense to you when you look at the code again?
All versions of the code will work. Which one to choose is often a personal or sometimes a team preference. What reads best for you? What will make most sense to you when you look at the code again?

[hexdocs-booleans]: https://hexdocs.pm/elixir/basic-types.html#booleans-and-nil
[hexdocs-truthy]: https://hexdocs.pm/elixir/Kernel.html#module-truthy-and-falsy-values
Expand Down

0 comments on commit c1694a5

Please sign in to comment.