Minimum Viable Code

The previous log had a surprisingly strong answer, with two feedback points:* 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 ended 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 knows 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 types cannot be composed, opaque body-based, complexBoth are solving the problem, but the first one has a modular design with strong boundaries, enabling less reckless refactorings.