Skip to content

Commit 89738ce

Browse files
Write approaches for Yacht (#3420)
* write approaches for yacht * improve wording, structure, and code * update based on bethany's changes * Apply suggestions from code review Co-authored-by: BethanyG <[email protected]> * improve snippets, add spm approach * Apply suggestions from code review --------- Co-authored-by: BethanyG <[email protected]>
1 parent cb75dfa commit 89738ce

File tree

8 files changed

+306
-0
lines changed

8 files changed

+306
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
{
2+
"introduction": {
3+
"authors": ["safwansamsudeen"]
4+
},
5+
"approaches": [
6+
{
7+
"uuid": "3593cfe3-5cab-4141-b0a2-329148a66bb6",
8+
"slug": "functions",
9+
"title": "Lambdas with Functions",
10+
"blurb": "Use lambdas with functions",
11+
"authors": ["safwansamsudeen"]
12+
},
13+
{
14+
"uuid": "eccd1e1e-6c88-4823-9b25-944eccaa92e7",
15+
"slug": "if-structure",
16+
"title": "If structure",
17+
"blurb": "Use an if structure",
18+
"authors": ["safwansamsudeen"]
19+
},
20+
{
21+
"uuid": "72079791-e51f-4825-ad94-3b7516c631cc",
22+
"slug": "structural-pattern-matching",
23+
"title": "Structural Pattern Matching",
24+
"blurb": "Use structural pattern matching",
25+
"authors": ["safwansamsudeen"]
26+
}
27+
]
28+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
## Approach: Using Lambdas with Functions
2+
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.
3+
4+
In `score`, we call the category (as it now points to a function) passing in `dice` as an argument.
5+
6+
```python
7+
def digits(num):
8+
return lambda dice: dice.count(num) * num
9+
10+
YACHT = lambda dice: 50 if len(set(dice)) == 1 else 0
11+
ONES = digits(1)
12+
TWOS = digits(2)
13+
THREES = digits(3)
14+
FOURS = digits(4)
15+
FIVES = digits(5)
16+
SIXES = digits(6)
17+
FULL_HOUSE = lambda dice: sum(dice) if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3] else 0
18+
FOUR_OF_A_KIND = lambda dice: 4 * dice[1] if dice[0] == dice[3] or dice[1] == dice[4] else 0
19+
LITTLE_STRAIGHT = lambda dice: 30 if sorted(dice) == [1, 2, 3, 4, 5] else 0
20+
BIG_STRAIGHT = lambda dice: 30 if sorted(dice) == [2, 3, 4, 5, 6] else 0
21+
CHOICE = sum
22+
23+
def score(dice, category):
24+
return category(dice)
25+
```
26+
27+
28+
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.
29+
30+
For `LITTLE_STRAIGHT` and `BIG_STRAIGHT`, we first sort the dice and then check it against the hard-coded value.
31+
Another way to solve this would be to check if `sum(dice) == 20 and len(set(dice)) == 5` (15 in `LITTLE_STRAIGHT`).
32+
In `CHOICE`, `lambda number : sum(number)` is shortened to just `sum`.
33+
34+
In `FULL_HOUSE`, we create a `set` to remove the duplicates and check the set's length along with the individual counts.
35+
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.
36+
37+
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.
38+
Additionally, [PEP8][pep8] does not recommend assigning constant or variable names to `lambda` expressions, so it is a better practice to use `def`:
39+
```python
40+
def digits(num):
41+
return lambda dice: dice.count(num) * num
42+
43+
def YACHT(dice): return 50 if len(set(dice)) == 1 else 0
44+
ONES = digits(1)
45+
TWOS = digits(2)
46+
THREES = digits(3)
47+
FOURS = digits(4)
48+
FIVES = digits(5)
49+
SIXES = digits(6)
50+
def FULL_HOUSE(dice): return sum(dice) if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3] else 0
51+
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
52+
def LITTLE_STRAIGHT(dice): return 30 if sorted(dice) == [1, 2, 3, 4, 5] else 0
53+
def BIG_STRAIGHT(dice): return 30 if sorted(dice) == [2, 3, 4, 5, 6] else 0
54+
CHOICE = sum
55+
56+
def score(dice, category):
57+
return category(dice)
58+
```
59+
60+
As you can see from the examples, the [ternary operator][ternary-operator] (_or ternary form_) is crucial in solving the exercise using one liners.
61+
As functions are being used, it might be a better strategy to spread the code over multiple lines to improve readability.
62+
```python
63+
def YACHT(dice):
64+
if dice.count(dice[0]) == len(dice):
65+
return 50
66+
return 0
67+
```
68+
69+
[closures]: https://www.programiz.com/python-programming/closure
70+
[ternary-operator]: https://www.tutorialspoint.com/ternary-operator-in-python
71+
[lambda]: https://docs.python.org/3/howto/functional.html?highlight=lambda#small-functions-and-the-lambda-expression
72+
[pep8]: https://peps.python.org/pep-0008/
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
def digits(num):
2+
return lambda dice: dice.count(num) * num
3+
YACHT = lambda x: 50 if x.count(x[0]) == len(x) else 0
4+
ONES = digits(1)
5+
FULL_HOUSE = lambda x: sum(x) if len(set(x)) == 2 and x.count(x[0]) in [2, 3] else 0
6+
LITTLE_STRAIGHT = lambda x: 30 if sorted(x) == [1, 2, 3, 4, 5] else 0
7+
def score(dice, category):
8+
return category(dice)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# If structure
2+
3+
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.
4+
5+
As one-liners aren't necessary here, we can spread out the code to make it look neater:
6+
```python
7+
ONES = 1
8+
TWOS = 2
9+
THREES = 3
10+
FOURS = 4
11+
FIVES = 5
12+
SIXES = 6
13+
FULL_HOUSE = 'FULL_HOUSE'
14+
FOUR_OF_A_KIND = 'FOUR_OF_A_KIND'
15+
LITTLE_STRAIGHT = 'LITTLE_STRAIGHT'
16+
BIG_STRAIGHT = 'BIG_STRAIGHT'
17+
CHOICE = 'CHOICE'
18+
YACHT = 'YACHT'
19+
20+
def score(dice, category):
21+
if category in (1,2,3,4,5,6):
22+
return dice.count(category) * category
23+
elif category == 'FULL_HOUSE':
24+
if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3]:
25+
return sum(dice) or 0
26+
elif category == 'FOUR_OF_A_KIND':
27+
if dice[0] == dice[3] or dice[1] == dice[4]:
28+
return dice[1] * 4 or 0
29+
elif category == 'LITTLE_STRAIGHT':
30+
if sorted(dice) == [1, 2, 3, 4, 5]:
31+
return 30 or 0
32+
elif category == 'BIG_STRAIGHT':
33+
if sorted(dice) == [2, 3, 4, 5, 6]:
34+
return 30 or 0
35+
elif category == 'YACHT':
36+
if all(num == dice[0] for num in dice):
37+
return 50
38+
elif category == 'CHOICE':
39+
return sum(dice)
40+
return 0
41+
```
42+
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.
43+
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).
44+
45+
This may not be an ideal way to solve the exercise, as the code is rather long and convoluted.
46+
However, it is a valid (_and fast_) solution.
47+
Using [structural pattern matching][structural pattern matching], introduced in Python 3.10, could shorten and clarify the code in this situation.
48+
Pulling some logic out of the `score` function and into additional "helper" functions could also help.
49+
50+
[structural pattern matching]: https://peps.python.org/pep-0636/
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ONES = 1
2+
YACHT = 'YACHT'
3+
def score(dice, category):
4+
if category == 'ONES':
5+
...
6+
elif category == 'FULL_HOUSE':
7+
...
8+
return 0
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Introduction
2+
Yacht in Python can be solved in many ways. The most intuitive approach is to use an `if` structure.
3+
Alternatively, you can create functions and set their names to the constant names.
4+
5+
## General guidance
6+
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.
7+
While mapping generally reminds us of dictionaries, here the constants are global.
8+
This indicates that the most idiomatic approach is not using a `dict`.
9+
Adhering to the principles of DRY is important - don't repeat yourself if you can help it, especially in the `ONES` through `SIXES` categories!
10+
11+
## Approach: functions
12+
Each bit of functionality for each category can be encoded in a function, and the constant name set to that function.
13+
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.
14+
```python
15+
```python
16+
def digits(num):
17+
return lambda dice: dice.count(num) * num
18+
YACHT = lambda dice: 50 if dice.count(dice[0]) == len(dice) else 0
19+
ONES = digits(1)
20+
TWOS = digits(2)
21+
THREES = digits(3)
22+
FOURS = digits(4)
23+
FIVES = digits(5)
24+
SIXES = digits(6)
25+
FULL_HOUSE = lambda dice: sum(dice) if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3] else 0
26+
FOUR_OF_A_KIND = lambda dice: 4 * dice[1] if dice[0] == dice[3] or dice[1] == dice[4] else 0
27+
LITTLE_STRAIGHT = lambda dice: 30 if sorted(dice) == [1, 2, 3, 4, 5] else 0
28+
BIG_STRAIGHT = lambda dice: 30 if sorted(dice) == [2, 3, 4, 5, 6] else 0
29+
CHOICE = sum
30+
def score(dice, category):
31+
return category(dice)
32+
```
33+
This is a very succinct way to solve the exercise, although some one-liners get a little long.
34+
For more information on this approach, read [this document][approach-functions].
35+
36+
## Approach: if structure
37+
The constants can be set to random, null, or numeric values, and an `if` structure inside `score` determines the code to be executed.
38+
As one-liners aren't necessary here, we can spread out the code to make it look neater:
39+
```python
40+
ONES = 1
41+
TWOS = 2
42+
THREES = 3
43+
FOURS = 4
44+
FIVES = 5
45+
SIXES = 6
46+
FULL_HOUSE = 'FULL_HOUSE'
47+
FOUR_OF_A_KIND = 'FOUR_OF_A_KIND'
48+
LITTLE_STRAIGHT = 'LITTLE_STRAIGHT'
49+
BIG_STRAIGHT = 'BIG_STRAIGHT'
50+
CHOICE = 'CHOICE'
51+
YACHT = 'YACHT'
52+
def score(dice, category):
53+
if category in (1,2,3,4,5,6):
54+
return dice.count(category) * category
55+
elif category == 'FULL_HOUSE':
56+
if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3]:
57+
return sum(dice) or 0
58+
elif category == 'FOUR_OF_A_KIND':
59+
if dice[0] == dice[3] or dice[1] == dice[4]:
60+
return dice[1] * 4 or 0
61+
elif category == 'LITTLE_STRAIGHT':
62+
if sorted(dice) == [1, 2, 3, 4, 5]:
63+
return 30 or 0
64+
elif category == 'BIG_STRAIGHT':
65+
if sorted(dice) == [2, 3, 4, 5, 6]:
66+
return 30 or 0
67+
elif category == 'YACHT':
68+
if all(num == dice[0] for num in dice):
69+
return 50
70+
elif category == 'CHOICE':
71+
return sum(dice)
72+
return 0
73+
```
74+
Read more on this approach [here][approach-if-structure].
75+
76+
[approach-functions]: https://exercism.org/tracks/python/exercises/yacht/approaches/functions
77+
[approach-if-structure]: https://exercism.org/tracks/python/exercises/yacht/approaches/if-structure
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Structural Pattern Matching
2+
3+
Another very interesting approach is to use [structural pattern matching][structural pattern matching].
4+
Existing in Python since 3.10, this feature allows for neater code than traditional if structures.
5+
6+
By and large, we reuse the code from the [if structure approach][approach-if-structure].
7+
We set the constants to random values and check for them in the `match` structure.
8+
`category` is the "subject", and in every other line, we check it against a "pattern".
9+
```python
10+
ONES = 1
11+
TWOS = 2
12+
THREES = 3
13+
FOURS = 4
14+
FIVES = 5
15+
SIXES = 6
16+
FULL_HOUSE = 'FULL_HOUSE'
17+
FOUR_OF_A_KIND = 'FOUR_OF_A_KIND'
18+
LITTLE_STRAIGHT = 'LITTLE_STRAIGHT'
19+
BIG_STRAIGHT = 'BIG_STRAIGHT'
20+
CHOICE = 'CHOICE'
21+
YACHT = 'YACHT'
22+
23+
def score(dice, category):
24+
match category:
25+
case 1 | 2 | 3 | 4 | 5 | 6:
26+
return dice.count(category) * category
27+
case 'FULL_HOUSE' if len(set(dice)) == 2 and dice.count(dice[0]) in [2, 3]:
28+
return sum(dice)
29+
case 'FOUR_OF_A_KIND' if dice[0] == dice[3] or dice[1] == dice[4]:
30+
return dice[1] * 4
31+
case 'LITTLE_STRAIGHT' if sorted(dice) == [1, 2, 3, 4, 5]:
32+
return 30
33+
case 'BIG_STRAIGHT' if sorted(dice) == [2, 3, 4, 5, 6]:
34+
return 30
35+
case 'YACHT' if all(num == dice[0] for num in dice):
36+
return 50
37+
case 'CHOICE':
38+
return sum(dice)
39+
case _:
40+
return 0
41+
```
42+
For the first pattern, we utilize "or patterns", using the `|` operator.
43+
This checks whether the subject is any of the provided patterns.
44+
45+
In the next five patterns, we check an additional condition along with the pattern matching.
46+
Finally, we use the wildcard operator `_` to match anything.
47+
As the compiler checks the patterns (`case`s) in order, `return 0` will be executed if none of the other patterns match.
48+
49+
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 ...`.
50+
This will capture the category and lead to unexpected behavior.
51+
52+
This code is much clenaer than the corresponding `if` structure code.
53+
54+
[structural pattern matching]: https://peps.python.org/pep-0636/
55+
[approach-if-structure]: https://exercism.org/tracks/python/exercises/yacht/approaches/if-structure
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
ONES = 1
2+
YACHT = 'YACHT'
3+
def score(dice, category):
4+
match category:
5+
case 1 | 2 | 3 | 4 | 5 | 6:
6+
return dice.count(category) * category
7+
case _:
8+
return 0

0 commit comments

Comments
 (0)