Skip to content

Instantly share code, notes, and snippets.

@lucasdicioccio
Last active March 19, 2019 02:39
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lucasdicioccio/256f854a17320b13856b to your computer and use it in GitHub Desktop.
Save lucasdicioccio/256f854a17320b13856b to your computer and use it in GitHub Desktop.
What an immutable GildedRose would look like
-- An alternative GildedRose using an immutable approach.
-- Context on the GildedRose:
-- * http://blog.lunarlogic.io/2015/what-ive-learned-by-doing-the-gilded-rose-kata-4-refactoring-tips/
-- * my comment in the linked blog post
-- One could take this GildedTulip approach and encapsulate a similar "immutable API"
-- to re-create the "mutable API" of the GildedRose.
--
-- This example is written in Haskell. This example uses three notable features (available in other languages):
-- - pattern matching: a way to hide a lot of "if"
-- - partial application: a (huge) syntax improvement over lambdas which are available in most languages
-- - list comprehension: like in Python
module GildedTulip where
-----------------------------------------------------------------------------------
-- data types to scope the problem
--
-- The key idea of this whole program is that some features of an Item
-- can vary with time.
-- We capture this key idea with the TimeVarying type and illustrate the idea with two Item properties.
--
-- We avoid two design flaws that our end-users probably do not care about:
-- * representing the time varying values using mutation
-- * representing time a hidden property of the program execution (i.e., how many times we called the "clock tick")
-- Instead, we represent time explicitly and recognize that some properties of an Item vary with time.
-- I claim that such a design is closer to the feature end-users need: query what is the current quality of an Item.
-- The proposed design also allows to query in the future and in the past (modulo causality).
-- I believe that it is nice to get a feature for free and I can charge for it anyway.
-- Simple type aliasing to make reasoning with type signatures less ambiguous.
type Day = Int
type Quality = Int
type Name = String
type Sellable = Bool
-- TimeVarying value can be represented by a "computed value at a given date".
type TimeVarying a = Day -> a
-- An Item has a name, and two time-varying properties:
-- - whether we are allowed to sell the item
-- - the quality of the item
data Item = Item {
name :: Name
, sellable :: (TimeVarying Sellable)
, quality :: (TimeVarying Quality)
}
-----------------------------------------------------------------------------------
-- We need to construct items and the pain-point is expressing TimeVarying values.
-- We ease the pain with some DSL-like functions that implement or transform
-- TimeVarying behaviors in a readable way.
-- Most items have a "best-before date" after which they are not sellable.
bestBefore :: Day -> TimeVarying Sellable
bestBefore deadline = \now -> now <= deadline
-- Some items may have a "not-sellable before date" (e.g., you wait for a
-- license to sell fermented coconut water).
maturity :: Day -> TimeVarying Sellable
maturity deadline = \now -> now >= deadline
-- A few items are timeless and will always be sellabled assets.
timeless :: TimeVarying Sellable
timeless = const True
-- Most items' quality have bounds (between 0 and 50).
-- We thus provide a function to cap a TimeVarying Quality.
--
-- The code uses a lambda to show explicitly that we are "returning a
-- TimeVarying Quality object" but we could also pass the argument of the lambda as
-- an extra argument of the capToBounds function and use partial application.
-- See the commented implementation. Both implementations are equivalent.
-- In later code, we will use the same syntax as in the commented implementation here.
capToBounds :: TimeVarying Quality -> TimeVarying Quality
-- capToBounds f x = let r = f x in max 0 (min r 50)
capToBounds f = \x -> let r = f x in max 0 (min r 50)
-- A few items are unbreakable and hence have a fixed quality.
unbreakable :: Quality -> TimeVarying Quality
unbreakable x = const x
-- We need a few different rules to compute the item quality of objects.
-- We provide four broad classes of objects:
-- * Basic items: degrade of 1 Quality per day until their deadline where they degrade of 2 Quality per day.
-- * Conjured items: degrade twice as fast as basic items.
-- * Brie-cheese: increase of 1 Quality per day until their deadline, then
-- taste improves of 2 Quality per day.
-- * Passes: tickets are worthless after sell deadline. Otherwise the quality
-- increases as the concert approaches. See implementation for the exact
-- equations.
--
-- Note that we use the trick of partial application that we illustrated in
-- `capToBounds` to improve readability. The capToBounds is better-understood
-- as an operation on a TimeVarying Quality value, here we un-alias the TimeVarying
-- to implement rules as quality in function of base quality, deadline, and current time.
-- an equivalent type for the 4 functions below is Quality -> Day -> Day -> Quality .
-- Pattern matching with guards make a good replacement of if/else.
basicItemQuality,conjuredItemQuality,brieQuality,passQuality ::
Quality -> Day -> TimeVarying Quality
basicItemQuality base deadline now
| deadline > now = base - now
| otherwise = (base - deadline) - 2 * (now - deadline)
conjuredItemQuality base deadline now
| deadline > now = base - 2*now
| otherwise = (base - deadline) - 4 * (now - deadline)
brieQuality base deadline now
| deadline > now = base + now
| otherwise = (base + deadline) + 2 * (now - deadline)
passQuality base deadline now
| deadline < now = 0
| deadline - now <= 5 = base + 3
| deadline - now <= 10 = base + 2
| otherwise = base
-----------------------------------------------------------------------------------
-- We now are equipped with the basic building blocks to build items.
sulfuras = Item "Sulfuras" timeless (unbreakable 60)
brie =
let deadline = 10
startQuality = 0
in Item "Brie" (bestBefore deadline) (capToBounds $ brieQuality startQuality deadline)
pass =
let deadline = 30
startQuality = 20
in Item "Pass" (bestBefore deadline) (capToBounds $ passQuality startQuality deadline)
armor =
let deadline = 40
startQuality = 30
in Item "Armor" (bestBefore deadline) (capToBounds $ basicItemQuality startQuality deadline)
pie =
let deadline = 4
startQuality = 10
in Item "Conjured pie" (bestBefore deadline) (capToBounds $ conjuredItemQuality startQuality deadline)
-- A function to compute all we need to sell a given Item at a given Day.
type AllWeNeedToKnow = (Day, Name, Sellable, Quality)
getInfo :: Item -> Day -> AllWeNeedToKnow
getInfo i x = (x, name i, sellable i x, quality i x)
-- A function to generate a string representation of a list of things we need to know to sell items.
format :: [AllWeNeedToKnow] -> String
format xs = unlines (map formatOne xs)
where formatOne (day, name, sellable, quality) =
unwords [show day, show name, show sellable, show quality]
-- An example program
example :: IO ()
example =
let inventory = [sulfuras, brie, brie, pass, armor, pie]
timeHorizon = [0 .. 100]
allWeNeedlist = [getInfo i t | i <- inventory, t <- timeHorizon]
in putStrLn $ format allWeNeedlist
{- Output of the example program:
0 "Sulfuras" True 60
1 "Sulfuras" True 60
2 "Sulfuras" True 60
3 "Sulfuras" True 60
4 "Sulfuras" True 60
5 "Sulfuras" True 60
6 "Sulfuras" True 60
7 "Sulfuras" True 60
8 "Sulfuras" True 60
9 "Sulfuras" True 60
10 "Sulfuras" True 60
11 "Sulfuras" True 60
12 "Sulfuras" True 60
13 "Sulfuras" True 60
14 "Sulfuras" True 60
15 "Sulfuras" True 60
16 "Sulfuras" True 60
17 "Sulfuras" True 60
18 "Sulfuras" True 60
19 "Sulfuras" True 60
20 "Sulfuras" True 60
21 "Sulfuras" True 60
22 "Sulfuras" True 60
23 "Sulfuras" True 60
24 "Sulfuras" True 60
25 "Sulfuras" True 60
26 "Sulfuras" True 60
27 "Sulfuras" True 60
28 "Sulfuras" True 60
29 "Sulfuras" True 60
30 "Sulfuras" True 60
31 "Sulfuras" True 60
32 "Sulfuras" True 60
33 "Sulfuras" True 60
34 "Sulfuras" True 60
35 "Sulfuras" True 60
36 "Sulfuras" True 60
37 "Sulfuras" True 60
38 "Sulfuras" True 60
39 "Sulfuras" True 60
40 "Sulfuras" True 60
41 "Sulfuras" True 60
42 "Sulfuras" True 60
43 "Sulfuras" True 60
44 "Sulfuras" True 60
45 "Sulfuras" True 60
46 "Sulfuras" True 60
47 "Sulfuras" True 60
48 "Sulfuras" True 60
49 "Sulfuras" True 60
50 "Sulfuras" True 60
51 "Sulfuras" True 60
52 "Sulfuras" True 60
53 "Sulfuras" True 60
54 "Sulfuras" True 60
55 "Sulfuras" True 60
56 "Sulfuras" True 60
57 "Sulfuras" True 60
58 "Sulfuras" True 60
59 "Sulfuras" True 60
60 "Sulfuras" True 60
61 "Sulfuras" True 60
62 "Sulfuras" True 60
63 "Sulfuras" True 60
64 "Sulfuras" True 60
65 "Sulfuras" True 60
66 "Sulfuras" True 60
67 "Sulfuras" True 60
68 "Sulfuras" True 60
69 "Sulfuras" True 60
70 "Sulfuras" True 60
71 "Sulfuras" True 60
72 "Sulfuras" True 60
73 "Sulfuras" True 60
74 "Sulfuras" True 60
75 "Sulfuras" True 60
76 "Sulfuras" True 60
77 "Sulfuras" True 60
78 "Sulfuras" True 60
79 "Sulfuras" True 60
80 "Sulfuras" True 60
81 "Sulfuras" True 60
82 "Sulfuras" True 60
83 "Sulfuras" True 60
84 "Sulfuras" True 60
85 "Sulfuras" True 60
86 "Sulfuras" True 60
87 "Sulfuras" True 60
88 "Sulfuras" True 60
89 "Sulfuras" True 60
90 "Sulfuras" True 60
91 "Sulfuras" True 60
92 "Sulfuras" True 60
93 "Sulfuras" True 60
94 "Sulfuras" True 60
95 "Sulfuras" True 60
96 "Sulfuras" True 60
97 "Sulfuras" True 60
98 "Sulfuras" True 60
99 "Sulfuras" True 60
100 "Sulfuras" True 60
0 "Brie" True 0
1 "Brie" True 1
2 "Brie" True 2
3 "Brie" True 3
4 "Brie" True 4
5 "Brie" True 5
6 "Brie" True 6
7 "Brie" True 7
8 "Brie" True 8
9 "Brie" True 9
10 "Brie" True 10
11 "Brie" False 12
12 "Brie" False 14
13 "Brie" False 16
14 "Brie" False 18
15 "Brie" False 20
16 "Brie" False 22
17 "Brie" False 24
18 "Brie" False 26
19 "Brie" False 28
20 "Brie" False 30
21 "Brie" False 32
22 "Brie" False 34
23 "Brie" False 36
24 "Brie" False 38
25 "Brie" False 40
26 "Brie" False 42
27 "Brie" False 44
28 "Brie" False 46
29 "Brie" False 48
30 "Brie" False 50
31 "Brie" False 50
32 "Brie" False 50
33 "Brie" False 50
34 "Brie" False 50
35 "Brie" False 50
36 "Brie" False 50
37 "Brie" False 50
38 "Brie" False 50
39 "Brie" False 50
40 "Brie" False 50
41 "Brie" False 50
42 "Brie" False 50
43 "Brie" False 50
44 "Brie" False 50
45 "Brie" False 50
46 "Brie" False 50
47 "Brie" False 50
48 "Brie" False 50
49 "Brie" False 50
50 "Brie" False 50
51 "Brie" False 50
52 "Brie" False 50
53 "Brie" False 50
54 "Brie" False 50
55 "Brie" False 50
56 "Brie" False 50
57 "Brie" False 50
58 "Brie" False 50
59 "Brie" False 50
60 "Brie" False 50
61 "Brie" False 50
62 "Brie" False 50
63 "Brie" False 50
64 "Brie" False 50
65 "Brie" False 50
66 "Brie" False 50
67 "Brie" False 50
68 "Brie" False 50
69 "Brie" False 50
70 "Brie" False 50
71 "Brie" False 50
72 "Brie" False 50
73 "Brie" False 50
74 "Brie" False 50
75 "Brie" False 50
76 "Brie" False 50
77 "Brie" False 50
78 "Brie" False 50
79 "Brie" False 50
80 "Brie" False 50
81 "Brie" False 50
82 "Brie" False 50
83 "Brie" False 50
84 "Brie" False 50
85 "Brie" False 50
86 "Brie" False 50
87 "Brie" False 50
88 "Brie" False 50
89 "Brie" False 50
90 "Brie" False 50
91 "Brie" False 50
92 "Brie" False 50
93 "Brie" False 50
94 "Brie" False 50
95 "Brie" False 50
96 "Brie" False 50
97 "Brie" False 50
98 "Brie" False 50
99 "Brie" False 50
100 "Brie" False 50
0 "Brie" True 0
1 "Brie" True 1
2 "Brie" True 2
3 "Brie" True 3
4 "Brie" True 4
5 "Brie" True 5
6 "Brie" True 6
7 "Brie" True 7
8 "Brie" True 8
9 "Brie" True 9
10 "Brie" True 10
11 "Brie" False 12
12 "Brie" False 14
13 "Brie" False 16
14 "Brie" False 18
15 "Brie" False 20
16 "Brie" False 22
17 "Brie" False 24
18 "Brie" False 26
19 "Brie" False 28
20 "Brie" False 30
21 "Brie" False 32
22 "Brie" False 34
23 "Brie" False 36
24 "Brie" False 38
25 "Brie" False 40
26 "Brie" False 42
27 "Brie" False 44
28 "Brie" False 46
29 "Brie" False 48
30 "Brie" False 50
31 "Brie" False 50
32 "Brie" False 50
33 "Brie" False 50
34 "Brie" False 50
35 "Brie" False 50
36 "Brie" False 50
37 "Brie" False 50
38 "Brie" False 50
39 "Brie" False 50
40 "Brie" False 50
41 "Brie" False 50
42 "Brie" False 50
43 "Brie" False 50
44 "Brie" False 50
45 "Brie" False 50
46 "Brie" False 50
47 "Brie" False 50
48 "Brie" False 50
49 "Brie" False 50
50 "Brie" False 50
51 "Brie" False 50
52 "Brie" False 50
53 "Brie" False 50
54 "Brie" False 50
55 "Brie" False 50
56 "Brie" False 50
57 "Brie" False 50
58 "Brie" False 50
59 "Brie" False 50
60 "Brie" False 50
61 "Brie" False 50
62 "Brie" False 50
63 "Brie" False 50
64 "Brie" False 50
65 "Brie" False 50
66 "Brie" False 50
67 "Brie" False 50
68 "Brie" False 50
69 "Brie" False 50
70 "Brie" False 50
71 "Brie" False 50
72 "Brie" False 50
73 "Brie" False 50
74 "Brie" False 50
75 "Brie" False 50
76 "Brie" False 50
77 "Brie" False 50
78 "Brie" False 50
79 "Brie" False 50
80 "Brie" False 50
81 "Brie" False 50
82 "Brie" False 50
83 "Brie" False 50
84 "Brie" False 50
85 "Brie" False 50
86 "Brie" False 50
87 "Brie" False 50
88 "Brie" False 50
89 "Brie" False 50
90 "Brie" False 50
91 "Brie" False 50
92 "Brie" False 50
93 "Brie" False 50
94 "Brie" False 50
95 "Brie" False 50
96 "Brie" False 50
97 "Brie" False 50
98 "Brie" False 50
99 "Brie" False 50
100 "Brie" False 50
0 "Pass" True 20
1 "Pass" True 20
2 "Pass" True 20
3 "Pass" True 20
4 "Pass" True 20
5 "Pass" True 20
6 "Pass" True 20
7 "Pass" True 20
8 "Pass" True 20
9 "Pass" True 20
10 "Pass" True 20
11 "Pass" True 20
12 "Pass" True 20
13 "Pass" True 20
14 "Pass" True 20
15 "Pass" True 20
16 "Pass" True 20
17 "Pass" True 20
18 "Pass" True 20
19 "Pass" True 20
20 "Pass" True 22
21 "Pass" True 22
22 "Pass" True 22
23 "Pass" True 22
24 "Pass" True 22
25 "Pass" True 23
26 "Pass" True 23
27 "Pass" True 23
28 "Pass" True 23
29 "Pass" True 23
30 "Pass" True 23
31 "Pass" False 0
32 "Pass" False 0
33 "Pass" False 0
34 "Pass" False 0
35 "Pass" False 0
36 "Pass" False 0
37 "Pass" False 0
38 "Pass" False 0
39 "Pass" False 0
40 "Pass" False 0
41 "Pass" False 0
42 "Pass" False 0
43 "Pass" False 0
44 "Pass" False 0
45 "Pass" False 0
46 "Pass" False 0
47 "Pass" False 0
48 "Pass" False 0
49 "Pass" False 0
50 "Pass" False 0
51 "Pass" False 0
52 "Pass" False 0
53 "Pass" False 0
54 "Pass" False 0
55 "Pass" False 0
56 "Pass" False 0
57 "Pass" False 0
58 "Pass" False 0
59 "Pass" False 0
60 "Pass" False 0
61 "Pass" False 0
62 "Pass" False 0
63 "Pass" False 0
64 "Pass" False 0
65 "Pass" False 0
66 "Pass" False 0
67 "Pass" False 0
68 "Pass" False 0
69 "Pass" False 0
70 "Pass" False 0
71 "Pass" False 0
72 "Pass" False 0
73 "Pass" False 0
74 "Pass" False 0
75 "Pass" False 0
76 "Pass" False 0
77 "Pass" False 0
78 "Pass" False 0
79 "Pass" False 0
80 "Pass" False 0
81 "Pass" False 0
82 "Pass" False 0
83 "Pass" False 0
84 "Pass" False 0
85 "Pass" False 0
86 "Pass" False 0
87 "Pass" False 0
88 "Pass" False 0
89 "Pass" False 0
90 "Pass" False 0
91 "Pass" False 0
92 "Pass" False 0
93 "Pass" False 0
94 "Pass" False 0
95 "Pass" False 0
96 "Pass" False 0
97 "Pass" False 0
98 "Pass" False 0
99 "Pass" False 0
100 "Pass" False 0
0 "Armor" True 30
1 "Armor" True 29
2 "Armor" True 28
3 "Armor" True 27
4 "Armor" True 26
5 "Armor" True 25
6 "Armor" True 24
7 "Armor" True 23
8 "Armor" True 22
9 "Armor" True 21
10 "Armor" True 20
11 "Armor" True 19
12 "Armor" True 18
13 "Armor" True 17
14 "Armor" True 16
15 "Armor" True 15
16 "Armor" True 14
17 "Armor" True 13
18 "Armor" True 12
19 "Armor" True 11
20 "Armor" True 10
21 "Armor" True 9
22 "Armor" True 8
23 "Armor" True 7
24 "Armor" True 6
25 "Armor" True 5
26 "Armor" True 4
27 "Armor" True 3
28 "Armor" True 2
29 "Armor" True 1
30 "Armor" True 0
31 "Armor" True 0
32 "Armor" True 0
33 "Armor" True 0
34 "Armor" True 0
35 "Armor" True 0
36 "Armor" True 0
37 "Armor" True 0
38 "Armor" True 0
39 "Armor" True 0
40 "Armor" True 0
41 "Armor" False 0
42 "Armor" False 0
43 "Armor" False 0
44 "Armor" False 0
45 "Armor" False 0
46 "Armor" False 0
47 "Armor" False 0
48 "Armor" False 0
49 "Armor" False 0
50 "Armor" False 0
51 "Armor" False 0
52 "Armor" False 0
53 "Armor" False 0
54 "Armor" False 0
55 "Armor" False 0
56 "Armor" False 0
57 "Armor" False 0
58 "Armor" False 0
59 "Armor" False 0
60 "Armor" False 0
61 "Armor" False 0
62 "Armor" False 0
63 "Armor" False 0
64 "Armor" False 0
65 "Armor" False 0
66 "Armor" False 0
67 "Armor" False 0
68 "Armor" False 0
69 "Armor" False 0
70 "Armor" False 0
71 "Armor" False 0
72 "Armor" False 0
73 "Armor" False 0
74 "Armor" False 0
75 "Armor" False 0
76 "Armor" False 0
77 "Armor" False 0
78 "Armor" False 0
79 "Armor" False 0
80 "Armor" False 0
81 "Armor" False 0
82 "Armor" False 0
83 "Armor" False 0
84 "Armor" False 0
85 "Armor" False 0
86 "Armor" False 0
87 "Armor" False 0
88 "Armor" False 0
89 "Armor" False 0
90 "Armor" False 0
91 "Armor" False 0
92 "Armor" False 0
93 "Armor" False 0
94 "Armor" False 0
95 "Armor" False 0
96 "Armor" False 0
97 "Armor" False 0
98 "Armor" False 0
99 "Armor" False 0
100 "Armor" False 0
0 "Conjured pie" True 10
1 "Conjured pie" True 8
2 "Conjured pie" True 6
3 "Conjured pie" True 4
4 "Conjured pie" True 6
5 "Conjured pie" False 2
6 "Conjured pie" False 0
7 "Conjured pie" False 0
8 "Conjured pie" False 0
9 "Conjured pie" False 0
10 "Conjured pie" False 0
11 "Conjured pie" False 0
12 "Conjured pie" False 0
13 "Conjured pie" False 0
14 "Conjured pie" False 0
15 "Conjured pie" False 0
16 "Conjured pie" False 0
17 "Conjured pie" False 0
18 "Conjured pie" False 0
19 "Conjured pie" False 0
20 "Conjured pie" False 0
21 "Conjured pie" False 0
22 "Conjured pie" False 0
23 "Conjured pie" False 0
24 "Conjured pie" False 0
25 "Conjured pie" False 0
26 "Conjured pie" False 0
27 "Conjured pie" False 0
28 "Conjured pie" False 0
29 "Conjured pie" False 0
30 "Conjured pie" False 0
31 "Conjured pie" False 0
32 "Conjured pie" False 0
33 "Conjured pie" False 0
34 "Conjured pie" False 0
35 "Conjured pie" False 0
36 "Conjured pie" False 0
37 "Conjured pie" False 0
38 "Conjured pie" False 0
39 "Conjured pie" False 0
40 "Conjured pie" False 0
41 "Conjured pie" False 0
42 "Conjured pie" False 0
43 "Conjured pie" False 0
44 "Conjured pie" False 0
45 "Conjured pie" False 0
46 "Conjured pie" False 0
47 "Conjured pie" False 0
48 "Conjured pie" False 0
49 "Conjured pie" False 0
50 "Conjured pie" False 0
51 "Conjured pie" False 0
52 "Conjured pie" False 0
53 "Conjured pie" False 0
54 "Conjured pie" False 0
55 "Conjured pie" False 0
56 "Conjured pie" False 0
57 "Conjured pie" False 0
58 "Conjured pie" False 0
59 "Conjured pie" False 0
60 "Conjured pie" False 0
61 "Conjured pie" False 0
62 "Conjured pie" False 0
63 "Conjured pie" False 0
64 "Conjured pie" False 0
65 "Conjured pie" False 0
66 "Conjured pie" False 0
67 "Conjured pie" False 0
68 "Conjured pie" False 0
69 "Conjured pie" False 0
70 "Conjured pie" False 0
71 "Conjured pie" False 0
72 "Conjured pie" False 0
73 "Conjured pie" False 0
74 "Conjured pie" False 0
75 "Conjured pie" False 0
76 "Conjured pie" False 0
77 "Conjured pie" False 0
78 "Conjured pie" False 0
79 "Conjured pie" False 0
80 "Conjured pie" False 0
81 "Conjured pie" False 0
82 "Conjured pie" False 0
83 "Conjured pie" False 0
84 "Conjured pie" False 0
85 "Conjured pie" False 0
86 "Conjured pie" False 0
87 "Conjured pie" False 0
88 "Conjured pie" False 0
89 "Conjured pie" False 0
90 "Conjured pie" False 0
91 "Conjured pie" False 0
92 "Conjured pie" False 0
93 "Conjured pie" False 0
94 "Conjured pie" False 0
95 "Conjured pie" False 0
96 "Conjured pie" False 0
97 "Conjured pie" False 0
98 "Conjured pie" False 0
99 "Conjured pie" False 0
100 "Conjured pie" False 0
-}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment