Extreme branchless: Cupcake functional style
In my previous log we had a look at the Cupcake kata.
I've mentioned that, this kata was thought to practice OOP's Design Pattern Decorator.
Which led to this design:
data Cake = Cake
{ symbol :: String
, price :: USDAmount
, toppings :: List String
}
cookie =
Cake {symbol="πͺ", price=200, toppings=mempty}
cupcake =
Cake {symbol="π§", price=100, toppings=mempty}
Then we can have a type for toppings, which wraps `Cake`:
type Topping = Cake -> Cake
mkTopping symbol' price' cake = cake { price = cake.price + price', toppings = cake.toppings <> [symbol']}
chocolate = mkTopping "π«" 10
nuts = mkTopping "π₯" 20
cakeName cake = cake.symbol <> toppingsName
where toppingsName = runList "" (\h t -> " with " <> h <> multipleToppings t) cake.toppings
multipleToppings = runList "" (\h t -> " and " <> h <> multipleToppings t)
While acceptable, we can argue that cakeName
:
- is branchless only thanks to
List
- is quite complex
The root of the problem comes from the constraint to implement an Object-Oriented Design (Pattern) in a functional language.
OOP has design patterns to workaround technical limitations, FP has mostly libraries working on structure (and few patterns).
Cake
does too much, let's start splitting it.
Cake
should be a basic cake:
data Cake = Cake
{ name :: String
, price :: USDAmount
}
cookie =
Cake {name="πͺ", price=200}
cupcake =
Cake {name="π§", price=100}
Then we have Toppings
, which represents toppings alone:
data Toppings = Toppings
{ name :: String
, price :: USDAmount
}
chocolate = Toppings {name="π«", price=10}
nuts = Toppings {name="π₯", price=20}
Finally, we need a way to combine them:
data CakeWithToppings = CakeWithToppings
{ name :: String
, price :: USDAmount
}
withToppings cake toppings = CakeWithToppings {name=cake.name <> " with " <> toppings.name, price=cake.price + toppings.price}
It was a bit brutal, if you hadn't noticied, we have moved from Topping
to Toppings
, this allows us to combine it through a Semigroup
:
x <> y = Toppings {name=x.name <> " and " <> y.name, price=x.price + y.price}
In short:
- Combining two
Toppings
adds a" and "
and gives a newToppings
- Combining a
Cake
with aToppings
adds a" with "
and gives aCakeWithToppings
Entirely type-safe (no risk to add multiple Toppings
or Cake
s together),
purely branchless, types-driving.
Notes: to give an idea of the usage, test-cases have been rewritten as such:
testCases =
[ mkTestCase "cupcake" cupcake "π§" 100
, mkTestCase "cookie" cookie "πͺ" 200
, mkTestCase "cupcake with chocolate" (cupcake `withToppings` chocolate) "π§ with π«" 110
, mkTestCase "cookie with chocolate and nuts" (cookie `withToppings` (chocolate <> nuts)) "πͺ with π« and π₯" 230
, mkTestCase "cookie with nuts and chocolate" (cookie `withToppings` (nuts <> chocolate)) "πͺ with π₯ and π«" 230
, mkTestCase "cookie with nuts" (cookie `withToppings` nuts) "πͺ with π₯" 220
]
Happy tasting!