Skip to content

Write approaches for Yacht #3420

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

Merged
merged 7 commits into from
Jun 5, 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
28 changes: 28 additions & 0 deletions exercises/practice/yacht/.approaches/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"introduction": {
"authors": ["safwansamsudeen"]
},
"approaches": [
{
"uuid": "3593cfe3-5cab-4141-b0a2-329148a66bb6",
"slug": "functions",
"title": "Lambdas with Functions",
"blurb": "Use lambdas with functions",
"authors": ["safwansamsudeen"]
},
{
"uuid": "eccd1e1e-6c88-4823-9b25-944eccaa92e7",
"slug": "if-structure",
"title": "If structure",
"blurb": "Use an if structure",
"authors": ["safwansamsudeen"]
},
{
"uuid": "72079791-e51f-4825-ad94-3b7516c631cc",
"slug": "structural-pattern-matching",
"title": "Structural Pattern Matching",
"blurb": "Use structural pattern matching",
"authors": ["safwansamsudeen"]
}
]
}
72 changes: 72 additions & 0 deletions exercises/practice/yacht/.approaches/functions/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
## Approach: Using Lambdas with Functions
Each bit of functionality for each category can be encoded in an anonymous function (otherwise known as a [`lambda` expression][lambda] or lambda form), and the constant name set to that function.

In `score`, we call the category (as it now points to a function) passing in `dice` as an argument.

```python
def digits(num):
return lambda dice: dice.count(num) * num

YACHT = lambda dice: 50 if len(set(dice)) == 1 else 0
ONES = digits(1)
TWOS = digits(2)
THREES = digits(3)
FOURS = digits(4)
FIVES = digits(5)
SIXES = digits(6)
FULL_HOUSE = lambda dice: sum(dice) if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3] else 0
FOUR_OF_A_KIND = lambda dice: 4 * dice[1] if dice[0] == dice[3] or dice[1] == dice[4] else 0
LITTLE_STRAIGHT = lambda dice: 30 if sorted(dice) == [1, 2, 3, 4, 5] else 0
BIG_STRAIGHT = lambda dice: 30 if sorted(dice) == [2, 3, 4, 5, 6] else 0
CHOICE = sum

def score(dice, category):
return category(dice)
```


Instead of setting each constant in `ONES` through `SIXES` to a separate function, we create a function `digits` that returns a function, using [closures][closures] transparently.

For `LITTLE_STRAIGHT` and `BIG_STRAIGHT`, we first sort the dice and then check it against the hard-coded value.
Another way to solve this would be to check if `sum(dice) == 20 and len(set(dice)) == 5` (15 in `LITTLE_STRAIGHT`).
In `CHOICE`, `lambda number : sum(number)` is shortened to just `sum`.

In `FULL_HOUSE`, we create a `set` to remove the duplicates and check the set's length along with the individual counts.
For `FOUR_OF_A_KIND`, we check if the first and the fourth element are the same or the second and the last element are the same - if so, there are (at least) four of the same number in the array.

This solution is a succinct way to solve the exercise, although some of the one-liners can get a little long and hard to read.
Additionally, [PEP8][pep8] does not recommend assigning constant or variable names to `lambda` expressions, so it is a better practice to use `def`:
```python
def digits(num):
return lambda dice: dice.count(num) * num

def YACHT(dice): return 50 if len(set(dice)) == 1 else 0
ONES = digits(1)
TWOS = digits(2)
THREES = digits(3)
FOURS = digits(4)
FIVES = digits(5)
SIXES = digits(6)
def FULL_HOUSE(dice): return sum(dice) if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3] else 0
def FOUR_OF_A_KIND(dice): return 4 * sorted(dice)[1] if len(set(dice)) < 3 and dice.count(dice[0]) in (1, 4, 5) else 0
def LITTLE_STRAIGHT(dice): return 30 if sorted(dice) == [1, 2, 3, 4, 5] else 0
def BIG_STRAIGHT(dice): return 30 if sorted(dice) == [2, 3, 4, 5, 6] else 0
CHOICE = sum

def score(dice, category):
return category(dice)
```

As you can see from the examples, the [ternary operator][ternary-operator] (_or ternary form_) is crucial in solving the exercise using one liners.
As functions are being used, it might be a better strategy to spread the code over multiple lines to improve readability.
```python
def YACHT(dice):
if dice.count(dice[0]) == len(dice):
return 50
return 0
```

[closures]: https://www.programiz.com/python-programming/closure
[ternary-operator]: https://www.tutorialspoint.com/ternary-operator-in-python
[lambda]: https://docs.python.org/3/howto/functional.html?highlight=lambda#small-functions-and-the-lambda-expression
[pep8]: https://peps.python.org/pep-0008/
8 changes: 8 additions & 0 deletions exercises/practice/yacht/.approaches/functions/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
def digits(num):
return lambda dice: dice.count(num) * num
YACHT = lambda x: 50 if x.count(x[0]) == len(x) else 0
ONES = digits(1)
FULL_HOUSE = lambda x: sum(x) if len(set(x)) == 2 and x.count(x[0]) in [2, 3] else 0
LITTLE_STRAIGHT = lambda x: 30 if sorted(x) == [1, 2, 3, 4, 5] else 0
def score(dice, category):
return category(dice)
50 changes: 50 additions & 0 deletions exercises/practice/yacht/.approaches/if-structure/content.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# If structure

The constants here can be set to random, null, or numeric values, and an `if` structure inside the `score` function can determine the code to be executed.

As one-liners aren't necessary here, we can spread out the code to make it look neater:
```python
ONES = 1
TWOS = 2
THREES = 3
FOURS = 4
FIVES = 5
SIXES = 6
FULL_HOUSE = 'FULL_HOUSE'
FOUR_OF_A_KIND = 'FOUR_OF_A_KIND'
LITTLE_STRAIGHT = 'LITTLE_STRAIGHT'
BIG_STRAIGHT = 'BIG_STRAIGHT'
CHOICE = 'CHOICE'
YACHT = 'YACHT'

def score(dice, category):
if category in (1,2,3,4,5,6):
return dice.count(category) * category
elif category == 'FULL_HOUSE':
if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3]:
return sum(dice) or 0
elif category == 'FOUR_OF_A_KIND':
if dice[0] == dice[3] or dice[1] == dice[4]:
return dice[1] * 4 or 0
elif category == 'LITTLE_STRAIGHT':
if sorted(dice) == [1, 2, 3, 4, 5]:
return 30 or 0
elif category == 'BIG_STRAIGHT':
if sorted(dice) == [2, 3, 4, 5, 6]:
return 30 or 0
elif category == 'YACHT':
if all(num == dice[0] for num in dice):
return 50
elif category == 'CHOICE':
return sum(dice)
return 0
```
Note that the code inside the `if` statements themselves can differ, but the key idea here is to use `if` and `elif` to branch out the code, and return `0` at the end if nothing else has been returned.
The `if` condition itself can be different, with people commonly checking if `category == ONES` as opposed to `category == 'ONES'` (or whatever the dummy value is).

This may not be an ideal way to solve the exercise, as the code is rather long and convoluted.
However, it is a valid (_and fast_) solution.
Using [structural pattern matching][structural pattern matching], introduced in Python 3.10, could shorten and clarify the code in this situation.
Pulling some logic out of the `score` function and into additional "helper" functions could also help.

[structural pattern matching]: https://peps.python.org/pep-0636/
8 changes: 8 additions & 0 deletions exercises/practice/yacht/.approaches/if-structure/snippet.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ONES = 1
YACHT = 'YACHT'
def score(dice, category):
if category == 'ONES':
...
elif category == 'FULL_HOUSE':
...
return 0
77 changes: 77 additions & 0 deletions exercises/practice/yacht/.approaches/introduction.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
# Introduction
Yacht in Python can be solved in many ways. The most intuitive approach is to use an `if` structure.
Alternatively, you can create functions and set their names to the constant names.

## General guidance
The main thing in this exercise is to map a category (_here defined as constants in the stub file_) to a function or a standalone piece of code.
While mapping generally reminds us of dictionaries, here the constants are global.
This indicates that the most idiomatic approach is not using a `dict`.
Adhering to the principles of DRY is important - don't repeat yourself if you can help it, especially in the `ONES` through `SIXES` categories!

## Approach: functions
Each bit of functionality for each category can be encoded in a function, and the constant name set to that function.
This can be done by assigning the constant name to a `lambda` or creating a one-line function using the constant as a function name.
```python
```python
def digits(num):
return lambda dice: dice.count(num) * num
YACHT = lambda dice: 50 if dice.count(dice[0]) == len(dice) else 0
ONES = digits(1)
TWOS = digits(2)
THREES = digits(3)
FOURS = digits(4)
FIVES = digits(5)
SIXES = digits(6)
FULL_HOUSE = lambda dice: sum(dice) if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3] else 0
FOUR_OF_A_KIND = lambda dice: 4 * dice[1] if dice[0] == dice[3] or dice[1] == dice[4] else 0
LITTLE_STRAIGHT = lambda dice: 30 if sorted(dice) == [1, 2, 3, 4, 5] else 0
BIG_STRAIGHT = lambda dice: 30 if sorted(dice) == [2, 3, 4, 5, 6] else 0
CHOICE = sum
def score(dice, category):
return category(dice)
```
This is a very succinct way to solve the exercise, although some one-liners get a little long.
For more information on this approach, read [this document][approach-functions].

## Approach: if structure
The constants can be set to random, null, or numeric values, and an `if` structure inside `score` determines the code to be executed.
As one-liners aren't necessary here, we can spread out the code to make it look neater:
```python
ONES = 1
TWOS = 2
THREES = 3
FOURS = 4
FIVES = 5
SIXES = 6
FULL_HOUSE = 'FULL_HOUSE'
FOUR_OF_A_KIND = 'FOUR_OF_A_KIND'
LITTLE_STRAIGHT = 'LITTLE_STRAIGHT'
BIG_STRAIGHT = 'BIG_STRAIGHT'
CHOICE = 'CHOICE'
YACHT = 'YACHT'
def score(dice, category):
if category in (1,2,3,4,5,6):
return dice.count(category) * category
elif category == 'FULL_HOUSE':
if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3]:
return sum(dice) or 0
elif category == 'FOUR_OF_A_KIND':
if dice[0] == dice[3] or dice[1] == dice[4]:
return dice[1] * 4 or 0
elif category == 'LITTLE_STRAIGHT':
if sorted(dice) == [1, 2, 3, 4, 5]:
return 30 or 0
elif category == 'BIG_STRAIGHT':
if sorted(dice) == [2, 3, 4, 5, 6]:
return 30 or 0
elif category == 'YACHT':
if all(num == dice[0] for num in dice):
return 50
elif category == 'CHOICE':
return sum(dice)
return 0
```
Read more on this approach [here][approach-if-structure].

[approach-functions]: https://exercism.org/tracks/python/exercises/yacht/approaches/functions
[approach-if-structure]: https://exercism.org/tracks/python/exercises/yacht/approaches/if-structure
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Structural Pattern Matching

Another very interesting approach is to use [structural pattern matching][structural pattern matching].
Existing in Python since 3.10, this feature allows for neater code than traditional if structures.

By and large, we reuse the code from the [if structure approach][approach-if-structure].
We set the constants to random values and check for them in the `match` structure.
`category` is the "subject", and in every other line, we check it against a "pattern".
```python
ONES = 1
TWOS = 2
THREES = 3
FOURS = 4
FIVES = 5
SIXES = 6
FULL_HOUSE = 'FULL_HOUSE'
FOUR_OF_A_KIND = 'FOUR_OF_A_KIND'
LITTLE_STRAIGHT = 'LITTLE_STRAIGHT'
BIG_STRAIGHT = 'BIG_STRAIGHT'
CHOICE = 'CHOICE'
YACHT = 'YACHT'

def score(dice, category):
match category:
case 1 | 2 | 3 | 4 | 5 | 6:
return dice.count(category) * category
case 'FULL_HOUSE' if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3]:
return sum(dice)
case 'FOUR_OF_A_KIND' if dice[0] == dice[3] or dice[1] == dice[4]:
return dice[1] * 4
case 'LITTLE_STRAIGHT' if sorted(dice) == [1, 2, 3, 4, 5]:
return 30
case 'BIG_STRAIGHT' if sorted(dice) == [2, 3, 4, 5, 6]:
return 30
case 'YACHT' if all(num == dice[0] for num in dice):
return 50
case 'CHOICE':
return sum(dice)
case _:
return 0
```
For the first pattern, we utilize "or patterns", using the `|` operator.
This checks whether the subject is any of the provided patterns.

In the next five patterns, we check an additional condition along with the pattern matching.
Finally, we use the wildcard operator `_` to match anything.
As the compiler checks the patterns (`case`s) in order, `return 0` will be executed if none of the other patterns match.

Note that the conditions might differ, but the patterns must have hard coded values - that is, you can't say `case ONES ...` instead of `case 1 ...`.
This will capture the category and lead to unexpected behavior.

This code is much clenaer than the corresponding `if` structure code.

[structural pattern matching]: https://peps.python.org/pep-0636/
[approach-if-structure]: https://exercism.org/tracks/python/exercises/yacht/approaches/if-structure
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
ONES = 1
YACHT = 'YACHT'
def score(dice, category):
match category:
case 1 | 2 | 3 | 4 | 5 | 6:
return dice.count(category) * category
case _:
return 0