Skip to content

Instantly share code, notes, and snippets.

@jmitchell
Last active June 9, 2018 22:55
Show Gist options
  • Save jmitchell/9299d52b41da4bd247e1b5915bcf28be to your computer and use it in GitHub Desktop.
Save jmitchell/9299d52b41da4bd247e1b5915bcf28be to your computer and use it in GitHub Desktop.
Avoid function argument ordering bugs by using the type system
{-
At the recent Seattle Haskell Learners' Group we touched on a common
programming bug, namely when a programmer mistakenly passes arguments
to a function in the wrong order. Let's consider an example of this
bug and ways to avoid it in Haskell.
Automated tests frequently need a mechanism to say, "the actual
computed value should equal some expected value." When the two values
differ the test framework should produce a useful message like,
FAILURE: Expected 5, but actual value was 6.
Which of these assertions produces that error message?
assertEquals(5, 2*3)
assertEquals(2*3, 5)
It's not obvious without referencing the test framework's
documentation. To make matters even more confusing and error-prone,
not all frameworks use the same argument order convention. Some
frameworks forego the problem by making the order insignificant and
the failure message less useful:
FAILURE: 5 is not equal to 6.
-}
module TestFrameworkDemo where
{-
Let's start with an error-prone `assertEquals` implementation in
Haskell where the expected value comes first.
-}
-- | Assert that an expected value is equal to an actual computed value
--
-- Examples:
--
-- >>> assertEquals 5 (2+3)
-- Right ()
--
-- >>> assertEquals 5 (2*3)
-- Left "FAILURE: Expected 5, but actual value was 6."
--
-- >>> assertEquals (2*3) 5
-- Left "FAILURE: Expected 6, but actual value was 5."
--
-- Note: To execute these examples as tests install doctest and run
-- `doctest TestFrameworkDemo.hs`.
assertEquals :: (Eq a, Show a) => a -> a -> Either String ()
assertEquals x y =
if x == y
then Right ()
else Left ("FAILURE: Expected " ++ show x ++ ", but actual value was " ++ show y ++ ".")
{-
Somehow we'd like the compiler help us prevent the argument-ordering
bug. The type checker verifies, at compile-time, that the program's
types are consistent, so we might be able to solve the problem using
types.
For now focus on the case where the type of the values, `a`, is
`Integer`. We can try using type aliases with the `type` keyword.
-}
type ExpectedInteger = Integer
type ActualInteger = Integer
typeAliasTest :: Either String ()
typeAliasTest = assertEquals second first -- notice the wrong order
where
first :: ExpectedInteger
first = 5
second :: ActualInteger
second = 2*3
{-
The type checker is happy with this. Type aliases won't do what we
want. As far as the type checker is concerned, `ExpectedInteger` and
`ActualInteger` are indistinguishable--they're the same as `Integer`.
Let's try using the `data` keyword.
-}
data ExpectedData a = Expected a
data ActualData a = Actual a
-- | data version of 'assertEquals'
--
-- Examples:
--
-- >>> assertEqualsData (Expected 5) (Actual (2+3))
-- Right ()
--
-- >>> assertEqualsData (Expected 5) (Actual (2*3))
-- Left "FAILURE: Expected 5, but actual value was 6."
--
-- >>> assertEqualsData (Actual (2*3)) (Expected 5)
-- EXPECTED ERROR
assertEqualsData :: (Eq a, Show a) => ExpectedData a -> ActualData a -> Either String ()
assertEqualsData (Expected x) (Actual y) =
if x == y
then Right ()
else Left ("FAILURE: Expected " ++ show x ++ ", but actual value was " ++ show y ++ ".")
{-
It works! By making explicit data type wrappers for the expected and
actual values and making the parameter ordering explicit in the
assertion function, programmers are much less likely to make the
argument-ordering mistake.
Since the data type is rather simple, you could consider using the
`newtype` keyword instead and get the same benefits. See
https://wiki.haskell.org/Newtype for more details on `newtype` and why
it's sometimes used instead of `data`. If the distinctions are
confusing, stick with `data` for now.
-}
newtype ExpectedNew a = Expected' a
newtype ActualNew a = Actual' a
-- | newtype version of 'assertEquals`
--
-- Examples:
--
-- >>> assertEqualsNew (Expected' 5) (Actual' (2+3))
-- Right ()
--
-- >>> assertEqualsNew (Expected' 5) (Actual' (2*3))
-- Left "FAILURE: Expected 5, but actual value was 6."
--
-- >>> assertEqualsNew (Actual' (2*3)) (Expected' 5)
-- EXPECTED ERROR
assertEqualsNew :: (Eq a, Show a) => ExpectedNew a -> ActualNew a -> Either String ()
assertEqualsNew (Expected' x) (Actual' y) =
if x == y
then Right ()
else Left ("FAILURE: Expected " ++ show x ++ ", but actual value was " ++ show y ++ ".")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment