Witness functions
Gautier DI FOLCO September 27, 2023 [dev] #haskell #design #engineering #refactoringOne of my favorite features of Haskell are the (G)ADTs, we could for example define a (not really) useful one:
data Expr
= Val Int
| Add Expr Expr
Doing so helps to easily define self-contained logic
=
\case
Val x -> x
Add x y -> eval x + eval y
eval
Usually we have write functions around them which go back and forth:
=
withObject "Expr" $ \o -> do
op <- o .: "op"
case op of
"val" -> Val <$> o .: "value"
"add" -> Add <$> o .: "op0" <*> o .: "op1"
_ -> fail $ "Unknown operator: '" <> op <> "'"
toJSON =
\case
Val x -> object ["op" .= ("val" :: Text), "value" .= x]
Add x y -> object ["op" .= ("add" :: Text), "op0" .= x, "op1" .= y]
parseJSON
If I add or change the constructors, I'll have a warning (or an error if properly configured)
on ToJSON
instance, hopefully I'll a test covering that, or, if piece of code are located
next to each other, I'll think of modifying FromJSON
.
But sometimes I have "one-way" functions:
=
\case
"val" -> Just $ Val 42
"add" -> Just $ Add (Val 1) (Val 1)
_ -> Nothing
example
Adding new constructors won't generate any warnings/errors, and tests won't catch it.
In these situations, I add a witness function below whose only purpose is to make the compilation break, forcing me to change the given function:
=
\case
Val _ -> ()
Add _ _ -> ()
_witnessExample