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

Raindrops 48in24 approaches #1422

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
# Pattern Matching
Copy link
Member

Choose a reason for hiding this comment

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

Wrong heading


```elixir
defmodule Raindrops do
@spec convert(pos_integer) :: String.t()
def convert(number) do
case {rem(number, 3), rem(number, 5), rem(number, 7)} do
{0, 0, 0} -> "PlingPlangPlong"
{0, 0, _} -> "PlingPlang"
{0, _, 0} -> "PlingPlong"
{_, 0, 0} -> "PlangPlong"
{0, _, _} -> "Pling"
{_, 0, _} -> "Plang"
{_, _, 0} -> "Plong"
_ -> Integer.to_string(number)
end
end
end
```

## Case
The case allows us to evaluate if a number is divisible by 3, 5 and 7 once, and then match the results to various combinations of the possible outcomes.
The advantage of using a `case` on a tuple, like in the example above, is that the `rem` functions are executed only once.

## Cond
We can use `cond do`, too.
However, first, let's look at the maths to make the solution more compact.

A number is divisible by `a`, `b`, and `c` only when it is divisible by `a*b*c`.
So, instead of
```elixir
rem(number, 3) == 0 and rem(number, 5) == 0 and rem(number, 7) == 0
```
we can write
```elixir
rem(number, 3*5*7) == 0
```

Now, let's look at this pattern matching with `cond`.

```elixir
def convert(number) do
cond do
rem(number, 3*5*7) == 0 -> "PlingPlangPlong"
rem(number, 3*5) == 0 -> "PlingPlang"
rem(number, 3*7) == 0 -> "PlingPlong"
rem(number, 5*7) == 0 -> "PlangPlong"
rem(number, 3) == 0 -> "Pling"
rem(number, 5) == 0 -> "Plang"
rem(number, 7) == 0 -> "Plong"
true -> Integer.to_string(number)
end
end
```

## Multiple-clause functions
We can do something very similar by using guards in multi-clause functions.
We use different feautre of the language, but at its core, the approach is the same.

```elixir
defmodule Raindrops do
@spec convert(pos_integer) :: String.t()
def convert(number) when rem(number, 3*5*7) == 0, do: "PlingPlangPlong"
def convert(number) when rem(number, 3*5) == 0, do: "PlingPlang"
def convert(number) when rem(number, 3*7) == 0, do: "PlingPlong"
michalporeba marked this conversation as resolved.
Show resolved Hide resolved
def convert(number) when rem(number, 5*7) == 0, do: "PlangPlong"
def convert(number) when rem(number, 3) == 0, do: "Pling"
def convert(number) when rem(number, 5) == 0, do: "Plang"
def convert(number) when rem(number, 7) == 0, do: "Plong"
def convert(number), do: Integer.to_string(number)
end
```

We can use different features of the language, but at its core, the approach is the same.
We check a set of conditions that leads us to the exact answer.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
def convert(number) do
case {rem(number, 3), rem(number, 5), rem(number, 7)} do
{0, 0, 0} -> "PlingPlangPlong"
{0, 0, _} -> "PlingPlang"
{0, _, 0} -> "PlingPlong"
{_, 0, 0} -> "PlangPlong"
{0, _, _} -> "Pling"
{_, 0, _} -> "Plang"
{_, _, 0} -> "Plong"
_ -> Integer.to_string(number)
end
end
22 changes: 22 additions & 0 deletions exercises/practice/raindrops/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"approaches": [
{
michalporeba marked this conversation as resolved.
Show resolved Hide resolved
"uuid": "fbfbabb4-f4e6-4329-b2f5-a93e3199b809",
"slug": "check-every-possibility",
"title": "Every Possibility",
"blurb": "Check every possibility.",
"authors": [
"michalporeba"
]
},
{
"uuid": "925ccb59-3414-472b-9054-1cdfc5e44fad",
"slug": "step-by-step",
"title": "Step By Step",
"blurb": "Perform the checks one by one, step by step.",
"authors": [
"michalporeba"
]
}
]
}
49 changes: 49 additions & 0 deletions exercises/practice/raindrops/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Introduction
Copy link
Member

Choose a reason for hiding this comment

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

I think my preference would be to try to start the approaches documents with the "most reasonable" solution first, and explore the more exotic solutions further down. I know that's very subjective, but I hope we can agree on which one is the best 😁

I have a strong opinion for this exercise that the best solution uses a list that pairs divisors to sounds, and then iterates over them like this one:

https://exercism.org/tracks/elixir/exercises/raindrops/solutions/SaberCon

I don't know if you would qualify that as a variant of a "step by step" approach? You didn't mention anything with Enum yet. Maybe it deserves to be its own approach.

Btw. In this exercise it's important to note that maps in Elixir are not sorted. This solution for example https://exercism.org/tracks/elixir/exercises/raindrops/solutions/alkhulaifi has to sort the map first before iterating, which clearly suggests a list of two-tuples is a better data structure here.


## Check every possibility

The output of the `convert` method depends on three conditions which can be either true or false.
This gives only eight possibilities and we can check them all.

```elixir
def convert(number) do
case {rem(number, 3), rem(number, 5), rem(number, 7)} do
{0, 0, 0} -> "PlingPlangPlong"
{0, 0, _} -> "PlingPlang"
{0, _, 0} -> "PlingPlong"
{_, 0, 0} -> "PlangPlong"
{0, _, _} -> "Pling"
{_, 0, _} -> "Plang"
{_, _, 0} -> "Plong"
_ -> Integer.to_string(number)
end
Comment on lines +10 to +19
Copy link
Member

Choose a reason for hiding this comment

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

end
```

We can use a few Elixir features to do more or less the same and we explore them in the [check every possibility approach][check-every-possibility-approach].

## Step by step

An alternative approach is to consider each condition one at a time.
At each step we return either a sound (i.e. "Pling", "Plang", or "Plong"), or an empty string.
We can then concatenate the strings together.

```elixir
def convert(number) do
pling = if rem(number, 3) == 0, do: "Pling", else: ""
plang = if rem(number, 5) == 0, do: "Plang", else: ""
plong = if rem(number, 7) == 0, do: "Plong", else: ""
result = pling <> plang <> plong

if result == "" do
Integer.to_string(number)
else
result
end
end
```

Let's have a look at a few variations of this [step by step approach][step-by-step-approach].

[check-every-possibility-approach]: https://exercism.org/tracks/elixir/exercises/raindrops/approaches/check-every-possibility
[step-by-step-approach]: https://exercism.org/tracks/elixir/exercises/raindrops/approaches/step-by-step
77 changes: 77 additions & 0 deletions exercises/practice/raindrops/.approaches/step-by-step/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Step By Step

```elixir
defmodule Raindrops do
@spec convert(pos_integer) :: String.t()
def convert(number) do
pling = if rem(number, 3) == 0, do: "Pling", else: ""
plang = if rem(number, 5) == 0, do: "Plang", else: ""
plong = if rem(number, 7) == 0, do: "Plong", else: ""
sound = pling <> plang <> plong

if sound == "" do
Integer.to_string(number)
else
sound
end
end
end
```

In this approach, we test each condition only once, similar to using the `case` on a tuple in the [pattern matching approach][pattern-matching-approach].
However, this time, if a condition is true, we capture the sound component, and if it is not true, we capture the sound as an empty string.

Once this is done, we can concatenate all three conditions to get the full sound.
Finally, if the `sound` is empty, we can return the number or, alternatively, the calculated `sound`.

## Functions

We can create private functions to test for component sounds.

```elixir
defp pling(n) when rem(n, 3) == 0, do: "Pling"
defp pling(_), do: ""
defp plang(n) when rem(n, 5) == 0, do: "Plang"
defp plang(_), do: ""
defp plong(n) when rem(n, 7) == 0, do: "Plong"
defp plong(_), do: ""
defp sound(sound, number) when sound == "", do: Integer.to_string(number)
defp sound(sound, _number), do: sound
```

Now the solution can look like this:
```elixir
def convert(number) do
sound(pling(number) <> plang(number) <> plong(number), number)
end
```
Comment on lines +1 to +47
Copy link
Member

Choose a reason for hiding this comment

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

String concatenation isn't used by Elixir devs that often. It has a big disadvantage - it crashes on nils. Elixir devs usually always use string interpolation.

Using string interpolation in both of those solutions would make them slightly shorter:

defmodule Raindrops do
  @spec convert(pos_integer) :: String.t()
  def convert(number) do
    pling = if rem(number, 3) == 0, do: "Pling"
    plang = if rem(number, 5) == 0, do: "Plang"
    plong = if rem(number, 7) == 0, do: "Plong"
    sound = "#{pling}#{plang}#{plong}"

    if sound == "" do
      Integer.to_string(number)
    else
      sound
    end
  end
end
defp pling(n), do: if(rem(n, 3) == 0, do: "Pling")
defp plang(n), do: if(rem(n, 5) == 0, do: "Plang")
defp plong(n), do: if(rem(n, 7) == 0, do: "Plong")
defp sound(sound, number) when sound == "", do: Integer.to_string(number)
defp sound(sound, _number), do: sound

def convert(number) do
  sound("#{pling(number)}#{plang(number)}#{plong(number)}", number)
end


## The pipe operator

With a slightly different design of the functions we can use the pipe operator to have a very clean-looking code

```elixir
@spec convert(pos_integer) :: String.t()
def convert(number) do
{"", number}
|> pling
|> plang
|> plong
|> sound
end
```
At least in the `convert` functions. The `pling`, `plang`, `plong` become a bit more complex:
```elixir
defp pling({ s, n }) when rem(n, 3) == 0, do: { s <> "Pling", n }
defp pling({ s, n }), do: { s, n }
defp plang({ s, n }) when rem(n, 5) == 0, do: { s <> "Plang", n }
defp plang({ s, n }), do: { s, n }
defp plong({ s, n }) when rem(n, 7) == 0, do: { s <> "Plong", n }
defp plong({ s, n }), do: { s, n }
defp sound({ s, n }) when s == "" , do: n |> Integer.to_string
defp sound({ s, _ }), do: s
```

All the examples above, at their core, represent the same approach of doing the check step by step.

[pattern-matching-approach]: https://exercism.org/tracks/elixir/exercises/raindrops/approaches/pattern-matching
Copy link
Member

Choose a reason for hiding this comment

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

What's the pattern matching approach for this exercise?

12 changes: 12 additions & 0 deletions exercises/practice/raindrops/.approaches/step-by-step/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
def convert(number) do
pling = if rem(number, 3) == 0, do: "Pling", else: ""
plang = if rem(number, 5) == 0, do: "Plang", else: ""
plong = if rem(number, 7) == 0, do: "Plong", else: ""
result = pling <> plang <> plong

if result == "" do
Integer.to_string(number)
else
result
end
end
3 changes: 2 additions & 1 deletion exercises/practice/raindrops/.meta/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@
"parkerl",
"sotojuan",
"Teapane",
"waiting-for-dev"
"waiting-for-dev",
"michalporeba"
],
"files": {
"solution": [
Expand Down
Loading