Let's discover Haskell and pattern matching via basic examples similar to "Let's play with pattern matching in Scala".
Step #1: create an enumeration
We can create an enumeration that represent the four suites in French playing cards:
data CardSuite = Club | Diamond | Heart | Spade
deriving (Eq, Enum, Show)
We have just created our own data type which:
- has four constructors (value constructors)
- inherits from Haskell's base types:
Eq
in order to know if two values are equal or notEnum
so that all values are known and ordered (sequentially ordered types)Show
so that we can have a string representation for debugging/troubleshooting
We can then use ghci
(Glascow Haskell Compiler Interactive environment), the Haskell REPL, to illustrate how we can use this enumeration:
*Main> Heart == Heart
True
*Main> Heart < Spade
True
*Main> succ Heart
Spade
Pattern matching examples
Example #1
The following function returns the Unicode symbol for a given card suite:
symbol :: CardSuite -> String
symbol cardSuite =
case cardSuite of
Club -> "♣"
Diamond ->"♦"
Heart -> "♥"
Spade -> "♠"
Let's evaluate it :
*Main> putStrLn $ symbol $ Heart
♥
Notice that:
- The
$
operator allows is a way chain function calls, omitting to use nested parenthesis (putStrLn(symbol(Heart))
). - the
putStrLn
standard function can display Unicode characters, whereas the standard functionshow
only displays ASCII characters. 😎
Morevover, the Haskell compiler can detect a non-exhaustive pattern matching. For instance, the following code :
symbol :: CardSuite -> String
symbol cardSuite =
case cardSuite of
Club -> "♣"
generates a compile-time warning :
warning: [-Wincomplete-patterns]
Pattern match(es) are non-exhaustive
In a case alternative:
Patterns not matched:
Diamond
Heart
Spade
and the following evaluation triggers an error :
*Main> symbol Diamond
"*** Exception: test-en.hs:(13,3)-(14,15): Non-exhaustive patterns in case
Example #2, share expression with a 'where' block
Let's implement a color
function that returns "red" or "black" depending on the input card suite:
color :: CardSuite -> String
color cardSuite =
case cardSuite of
Club -> black
Diamond -> red
Heart -> red
Spade -> black
where
red = "red"
black = "black"
Let's evaluate it:
*Main> color Heart
"red"
The where
keyword is used there to share some expressions.
Example #3: destructuring
Let's say we want to define our custom type Card
that combines a rank (1, 2, 3, ..., Jack, Queen, King) and a suite:
data Rank =
R1 | R2 | R3 | R4 | R5 | R6 | R7 | R8 | R9 | R10 | Jack | Queen | King
deriving (Eq, Ord, Enum, Show)
data Card = Card {
rank :: Rank,
suite :: CardSuite
} deriving (Eq, Show)
The Card
type uses the record syntax that allows to name fields.
We can then use pattern matching in order to de-structure a card, filtering some fields. For instance, the following function determines if two cards have the same suite:
sameSuite :: (Card, Card) -> Bool
sameSuite ((Card _ suite1), (Card _ suite2)) =
suite1 == suite2
Call examples:
*Main> :{
*Main| sameSuite (
*Main| Card {rank=R1, suite=Diamond},
*Main| Card {rank=R1, suite=Heart} )
*Main| :}
False
*Main> :{
*Main| sameSuite (
*Main| Card {rank=Jack, suite=Heart},
*Main| Card {rank=R1, suite=Diamond} )
*Main| :}
False
Card ranks, that are not needed by our function, have been filtered using the wild-card symbol (_
).
That's all, folks! 🤓