Minimum Viable Code

The previous log had a surprising strong answer, with two feedbacks:

  • Nolwenn spotted a weakness in the tests
  • Anton was more direct, arguing I had dodged the kata

As a reminder, we have reached the following code:

data Stats = Stats
  { diet :: Diet,
    organic :: Organic,
    fat :: Fat,
    salt :: Salt,
    calories :: Calories
  }
  deriving stock (Eq, Show, Generic)
  deriving (Semigroup, Monoid) via (Generically Stats)

data Diet
  = Pescetarian
  | Vegetarian
  | Vegan
  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
  deriving (Semigroup, Monoid) via (Min Diet)

data Organic
  = NonOrganic
  | Organic
  deriving stock (Eq, Ord, Show, Enum, Bounded, Generic)
  deriving (Semigroup, Monoid) via (Min Organic)

newtype Fat
  = Fat Natural
  deriving newtype (Eq, Ord, Num, Show)
  deriving (Semigroup, Monoid) via (Sum Fat)

newtype Salt
  = Salt Natural
  deriving newtype (Eq, Ord, Num, Show)
  deriving (Semigroup, Monoid) via (Sum Salt)

newtype Calories
  = Calories Natural
  deriving newtype (Eq, Ord, Num, Show)
  deriving (Semigroup, Monoid) via (Sum Calories)

If I had to follow the intent of the intent of the kata, I would have end up with the following snippet:

data Stats' = Stats'
  { diet :: Diet,
    organic :: Organic,
    fat :: Natural,
    salt :: Natural,
    calories :: Natural
  }
  deriving stock (Eq, Show, Generic)

instance Semigroup Stats' where
  x <> y =
    Stats'
      { diet = min x.diet y.diet,
        organic = min x.organic y.organic,
        fat = x.fat + y.fat,
        salt = x.salt + y.salt,
        calories = x.calories + y.calories
      }

instance Monoid Stats' where
  mempty =
    Stats'
      { diet = Vegan,
        organic = Organic,
        fat = 0,
        salt = 0,
        calories = 0
      }

Before highlighting the trade-offs, we should explore what I call minimum viable code.

When I review code, I focus on one thing: refactorability.

Nobody should expect a piece of code to stay unchanged, so every piece of should be evaluated to its ability to change.

So I look at:

  • Architecture: coupling and cohesion, trade-offs, data management, layering
  • Code structure: type-safety, composability of the interface, structure-based vs opaque body-based, composed vs complected

Let's analyze the first version:

  • Architecture: each instance only know its type definition
  • Code structure: strongly typed, each type can be composed and reused, structure-based, composed

And the second one:

  • Architecture: only one instance of each
  • Code structure: weakly typed, reused typed cannot be composed, opaque body-based, complected

Both are solving the problem, but the first one has a modular design with strong boundaries, enabling reckless refactorings.