DEV Community

Bruno Dias
Bruno Dias

Posted on

Modeling algorithms with types

[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
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

interface ApplyDiscount {
  applyDiscount(discount: Discount): ApplyDiscount;
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
// 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];
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode
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];
Enter fullscreen mode Exit fullscreen mode

Discussion (0)