haskelltypespattern-matchingtype-constructor

Technical implementation of pattern matching vs. guards


I was trying to solve a Haskell task writing a function that involves custom data types for playing cards. The function should take a card as argument and return the value of the card, which is based on its rank (Ace -> 11, Jack/Queen/King -> 10, Numbers 2-9 -> 2-9). The issue is basically needing to access the argument of a data constructor in case the card rank has a number value.

I found a solution with pattern matching and thanks to this and this thread I also found out how to implement it with case of and pattern guards (cardValueGuards).

I still wonder what makes pattern matching work the way it does on the technical level. And I wonder if one approach is better than another in this example. (See questions below.)

data Color = Red | Black deriving (Show, Eq)
data Suit = Clubs | Diamonds | Hearts | Spades deriving (Show, Eq)
data Rank = Num Int | Jack | Queen | King | Ace deriving (Show, Eq)
data Card = Card { suit :: Suit, rank :: Rank } deriving (Show, Eq)

-- Working solutions
cardValue :: Card -> Int
cardValue card = selectNum (rank card)
    where
        selectNum :: Rank -> Int
        selectNum Ace = 11
        selectNum (Num a) = a
        selectNum _ = 10

cardValueCase :: Card -> Int
cardValueCase card = case rank card of
    Ace -> 11
    (Num a) -> a
    otherwise -> 10

cardValueGuards :: Card -> Int
cardValueGuards card
    | rank card == Ace = 11
    | (Num a) <- rank card  = a
    | otherwise = 10


cardValueGuardsLong :: Card -> Int
cardValueGuardsLong card
    | rank card == Ace = 11
    | rank card == (Num 2) = 2
    | rank card == (Num 3) = 3
    | rank card == (Num 4) = 4
    | rank card == (Num 5) = 5
    | rank card == (Num 6) = 6
    | rank card == (Num 7) = 7
    | rank card == (Num 8) = 8
    | rank card == (Num 9) = 9
    | otherwise = 10

The below was my old attempt. (I suppose rank card == (Num a) = a fails because using the function (==) requires specific values instead of type variables.)

-- Not working
cardValueGuardsOld :: Card -> Int
cardValueGuardsOld card
    | rank card == Ace = 11
    | rank card == (Num a) = a
    | otherwise = 10

To elaborate: From my understanding, when using a data constructor to construct a value with Records, Haskell creates a function that returns the value associated with the name of the record. So creating a card with mycard = Card Spades Ace lets you access the card's rank by using rank mycard which returns Ace. And for mycard2 = Card Diamonds (Num 8) it would return Num 8.

And judging from the second thread about the difference between pattern matching and guards, guards are basically if-expressions, while pattern matching is like case of. In this answer it says the difference is that pattern matching only checks "whether a value was created using a given constructor" and that pattern matching binds variables.

Intuitively I would have understood selectNum (rank card) followed by selectNum (Num a) = a to basically do the same thing as | rank card == (Num a) = a. In both cardValue and in cardValueGuardsOld I get the rank of the card and then pass it on to check its value. Obviously something is different, but from what I've read so far I still don't understand why the pattern matching version is allowed while | rank card == (Num a) = a fails.


Solution

  • (I suppose rank card == (Num a) = a fails because using the function (==) requires specific values instead of type variables.)

    Yes. (Here a is a plain variable, though, not a type variable.)

    I still don't understand why the pattern matching version is allowed while | rank card == (Num a) = a fails.

    As you put it: because (==) is not special syntax, it's a function like many others. Writing x == y is the same as writing the function call (==) x y. Compare with the guard

    ...
    | myComparisonFunction (rank card) (Num a) = a
    

    This function call expects two expressions, rank card, and Num a as arguments. The second expression involves a variable a which was not defined before, so the compiler raises an error. The thumb rule is: the value of variables in an expression need to be known in order to compute the value for the expression. (This is partly true in recursive definitions, but let's ignore that.)

    By contrast, patterns define the values of variables inside them. If we match a value against a pattern, this can either fail (a constructor does not match) or succeed, and in the latter case all the variables in the pattern are bound to a value, defining them.

    So, while the syntax Num a can be used both as an expression and as a pattern, its semantics is very different between these cases.

    How exactly are pattern matching and case of implemented (on the compiler level)? What's the difference that allows them to work while the equality check in cardValueGuardsOld fails? Does it have to do with scope or is it because pattern matching only checks constructor functions or something else?

    It's all function call vs pattern matching, expressions vs patterns that makes the difference.

    You are also corrent when you mention that patterns can only involce constructors, and not general functions. We can't write

    f (g x y) = ...
    

    since g is not a constructor. We could instead write

    f (G x y) = ...
    

    for a constructor G.

    Also not that, for most types, the function (==) is implemented using patter matching anyway. Hence, using patterns directly instead of calling == is a good idea.

    There are also some advanced features of Haskell (like GADTs) which only work using patterns.

    Finally, using patterns avoids a common design pitfall known as "boolean blindness", where booleans are extensively used instead of richer types (e.g. Maybe a, Either a b). Focusing too much on booleans often leads to a poor Haskell code.

    Is the standard pattern matching/case of more or less preferable than using guards in this case or is it a matter of taste?

    While in some cases there might be a way to solve a task using both ways, pattern matching should be the "default" approach. Using == when pattern matching would work is not a good idea.

    Consider

    data T = A | B deriving Eq
    
    f :: T -> String
    f A = "a"
    f B = "b"
    
    g :: T -> String
    g x
      | x == A = "a"
      | x == B = "b"
    

    After turning warning on with -Wall, GHC can detect that f handles all cases, and does not generate any warning about it.

    This is not the case for g. GHC will warn that without a final otherwise case, all the guards could be false. Which is annoying.

    Further, what if we add a new constructor later on?

    data T = A | B | C deriving Eq
    

    Now we get an error on f, which is good since we need to fix the function for the new case. By contrast, g keeps compiling and will crash at runtime.

    And if I wanted to actually check that the number cards only have numbers from 2 to 9, would that make guards a better choice?

    I'd say so. If we want to check 10 <= x && x <= 1000 we can't seriously do that with 990 patterns.