Last active
June 9, 2018 22:55
-
-
Save jmitchell/9299d52b41da4bd247e1b5915bcf28be to your computer and use it in GitHub Desktop.
Avoid function argument ordering bugs by using the type system
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{- | |
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