[First draft]

Things get so much easier when you try

to understand how the types work together

to accomplish some work.

In this article, we are going to model

a system that apply discounts using many

strategies.

I'm going to use Haskell and javascript.

haskell (or ocaml if you like) is great to visualize

what's going on the type level...

We can starting by trying to think

on the simplest way to accomplish the job.

We can start by using a simple subtraction `(-)`

.

```
applyDiscount :: Float -> Float -> Float
applyDiscount price discount = price - discount
let price = 10.0
discounts = [1.0, 4.0]
finalPrice = 5.0
in foldl applyDiscount price discounts == finalPrice
```

```
const applyDiscount = (price, discount) => price - discount;
const price = 10.0;
const discounts = [1.0, 4.0];
const finalPrice = 5.0;
discounts.reduce(applyDiscount, price) == finalPrice;
```

Then we can clarify what which `Float`

means

on the type specifiction...

```
type Price = Float
type Discount = Float
applyDiscount :: Price -> Discount -> Price
applyDiscount price discount = price - discount
```

Now we have `Price -> Discount -> Price`

which means

"given a Price and a Discount, it returns back the price

after the discount"...and that means, in this case,

that the Price acts like an accumulator!

With this information, we can now improve `applyDiscount`

to be more generic.

```
class ApplyDiscount a where
applyDiscount :: a -> Discount -> a
applyDiscounts :: ApplyDiscount a => a -> [Discount] -> a
applyDiscounts = foldl applyDiscount
```

In this case, in typescript, it should be the same as...

```
interface ApplyDiscount {
applyDiscount(discount: Discount): ApplyDiscount;
}
```

Now, we just need to create instances for each `a`

!

```
newtype ApplyAll = ApplyAll Price
deriving (Show, Eq)
instance ApplyDiscount a where
applyDiscount (ApplyAll p) d = ApplyAll (p - d)
let discounts = [1.0, 4.0]
strategy = ApplyAll 10.0
finalPrice = ApplyAll 5.0
in foldl applyDiscount strategy discounts == finalPrice
```

```
class ApplyAll {
constructor(x) { this.price = x; }
applyDiscount(d) { return new ApplyAll(this.price - d); }
}
const discounts = [1.0, 4.0];
const strategy = new ApplyAll(10.0);
const finalPrice = 5.0;
const applyDiscount =
(strategy, discount) => strategy.applyDiscount(discount);
discounts.reduce(applyDiscount, strategy).price == finalPrice;
```

Now we have our first strategy! `ApplyAll`

just apply all discounts available,

and this is a great first strategy.

A new requirement has arived!

Now, we have too apply all discounts,

but we are not going to let the final price

goes negative.

```
newtype ApplyAllNoNegative = ApplyAllNoNegative Price
deriving (Show, Eq)
instance ApplyDiscount a where
applyDiscount (ApplyAllNoNegative p) d =
ApplyAllNoNegative (max 0 (p - d))
let discounts = [5.0, 5.0]
strategy = ApplyAllNoNegative 8.0
finalPrice = ApplyAllNoNegative 0.0
in foldl applyDiscount strategy discounts == finalPrice
```

```
class ApplyAllNoNegative {
constructor(x) { this.price = x; }
applyDiscount(d) {
const price = Math.max(0, this.price - d);
return new ApplyAllNoNegative(price);
}
}
const discounts = [1.0, 4.0];
const strategy = new ApplyAllNoNegative(10.0);
const finalPrice = 5.0;
discounts.reduce(applyDiscount, strategy).price == finalPrice;
```

Too easy!

Our next task is to:

"If it goes negative, just collect

all discounts that we couldn't apply,

otherwise return the final price."

```
newtype CollectDiscountWhenGoesNegative =
Applying Price |
NotApplied [Discount]
deriving (Show, Eq)
instance ApplyDiscount a where
applyDiscount (Applying p) d =
let x = p - d
in if x >= 0 then
Applying x
else
NotApplied [d]
applyDiscount (NotApplied ls) c =
NotApplied (c : ls)
let discounts = [5.0, 5.0]
strategy = Applying 10.0
finalPrice = Applying 0.0
in foldl applyDiscount strategy discounts == finalPrice
let discounts = [5.0, 8.0]
strategy = Applying 10.0
finalPrice = NotApplied [8.0]
in foldl applyDiscount strategy discounts == finalPrice
```

```
// Don't do this! It's just to help visualize what's going on.
class CollectDiscountWhenGoesNegative {}
class NotApplied extends CollectDiscountWhenGoesNegative {
constructor(x) { this.list = x; }
applyDiscount(d) { retun new this(this.list.concat([d])); }
}
class Applying extends CollectDiscountWhenGoesNegative {
constructor(x) { this.price = x; }
applyDiscount(d) {
const price = this.price - d;
if (price >= 0) {
return new Applying(price);
} else {
return new NotApplied([d]);
}
}
}
const discounts = [1.0, 4.0];
const strategy = new Applying(10.0);
const finalPrice = 5.0;
discounts.reduce(applyDiscount, strategy).price == finalPrice;
const discounts = [1.0, 4.0];
const strategy = new Applying(1.0);
discounts.reduce(applyDiscount, strategy).list == [4.0];
```

It's really easy to work with this. If

the discount goes negative, we know exactly

which discounts were not applied to mark on

the frontend as feedback.

Our final task is:

"It must not apply duplicate discounts,

and it must keep the duplicated to send as feedback

to the user. Also, it must use the previous strategies

so we can allow it goes negative or not."

```
type AlreadyAppliedDiscounts = S.Set Discount
type DuplicatedDiscounts = [Discount]
data DontApplyDuplicate a =
DontApplyDuplicate AlreadyAppliedDiscounts DuplicatedDiscounts a
deriving (Show, Eq)
dadup = DontApplyDuplicate
instance (ApplyDiscount a) => ApplyDiscount (DontApplyDuplicate a) where
applyDiscount (DontApplyDuplicate ls xs s) d =
if S.member d ls then
dadup ls (d : xs) s
else
dadup (S.insert d ls) xs $ applyDiscount s d
-- could be a `Default` instance
new = dadup S.empty []
let discounts = [5.0, 5.0]
strategy = new (Applying 10.0)
finalPrice = dadup (S.fromList [5.0]) [5.0] (Applying 5)
in foldl applyDiscount strategy discounts == finalPrice
let discounts = [5.0, 8.0]
strategy = Applying 10.0
finalPrice = NotApplied [8.0]
in foldl applyDiscount strategy discounts == finalPrice
```

```
class DontApplyDuplicate {
constructor(strategy) {
this.usedDiscounts = new Set();
this.duplicated = [];
this.strategy = strategy;
}
applyDiscount(d) {
if (this.usedDiscount.has(d)) {
this.duplicated.push(d);
return this;
}
this.usedDiscount.add(d);
this.strategy = this.strategy.applyDiscount(d)
return this;
}
}
const discounts = [1.0, 4.0];
const strategy = new DontApplyDuplicate(new Applying(10.0));
const finalPrice = 5.0;
discounts.reduce(
(p, d) => p.applyDiscount(d),
strategy
).price == finalPrice;
const discounts = [1.0, 1.0, 4.0, 6.0];
const strategy = new DontApplyDuplicate(new Applying(10.0));
const {
usedDiscounts,
duplicated,
strategy: { list },
} = discounts.reduce(applyDiscount, strategy);
usedDiscount = [1.0, 4.0];
duplicated = [1.0];
list = [6.0];
```

## Discussion (0)