Home > Software design >  Haskell 101 | What do the Deriving Keywords mean?
Haskell 101 | What do the Deriving Keywords mean?

Time:11-11

Essentially practically I'm trying to implement a custom data type that is a subset of the string class (e.g. so I can do some type checking for error input). But I cannot for the life of me find anywhere that just explains what each type keyword means. According to here: Haskell Docs these are the 7 basic ones: Eq, Ord, Enum, Ix, Bounded, Read, and Show. And aside from Show which is needed for print statements and Eq which needed for boolean style comparison checks, I'm not completely certain on the other 5, and google has not been very much help. So I'm hoping y'all can shed some light and maybe point me in the correct direction.

So questions:

  1. What are these 7 basic derives / what does adding them do?
  2. Is there a way within ghci I can run something like derives String or derives "abc"?

Here's the code I'm working on. Essentially I've just created this Card data type which as you can see is just a string that does some more strict parameter checks. And what I'm trying to do is pass it to match which previously accepted 3 strings as an argument (now 3 Cards). But in order to use the string manipulation syntax like breaking it down into a list of characters, I believe I need to identify the appropriate derive keyword so that it behaves in this manner.

match :: Card -> Card -> Card -> Bool
match [] [] [] = True
match [a] [b] [c] = (a == b && b == c)
  || (a /= b && b /= c && a /= c)
match (a:as) (b:bs) (c:cs) = match [a] [b] [c]
  && match as bs cs

data Card = Card String
            deriving (Show,Eq)
card :: String -> Card
card x | length x /= 4 = error "Card input must be 4 characters."
  | (x!!0 /= 'o') && (x!!0 /= 's') && (x!!0 /= 'd') = error "char 0 (shape) must be o s or d."
  | (x!!1 /= '1') && (x!!1 /= '2') && (x!!1 /= '3') = error "char 1 (number) must be 1 2 or 3."
  | (x!!2 /= 'r') && (x!!2 /= 'p') && (x!!2 /= 'g') = error "char 2 (color) must be r p or g."
  | (x!!3 /= 'f') && (x!!3 /= 's') && (x!!3 /= 'o') = error "char 3 (shade) must be f s or o."
  | otherwise = Card x

(Updated Example #1)

data Card = Card Shape Number Color Shade deriving (Show, Eq)

data Shape = Oval | Squiggle | Diamond deriving Eq
instance Show Shape where
    show Oval     = "Oval"
    show Squiggle = "Squiggle"
    show Diamond  = "Diamond"

data Number = One | Two | Three deriving Eq
instance Show Number where
    show One   = "One"
    show Two   = "Two"
    show Three = "Three"

data Color = Red | Pink | Green deriving Eq
instance Show Color where
    show Red   = "Red"
    show Pink  = "Pink"
    show Green = "Green"

data Shade = Full | Half | Empty deriving Eq
instance Show Shade where
    show Full  = "Full"
    show Half  = "Half"
    show Empty = "Empty"

shape :: Char -> Either ShapeError Shape
shape 'o' = Right Oval
shape 's' = Right Squiggle
shape 'd' = Right Diamond
shape x   = Left (NotOSD x)

number :: Char -> Either NumberError Number
number '1' = Right One
number '2' = Right Two
number '3' = Right Three
number x   = Left (Not123 x)

color :: Char -> Either ColorError Color
color 'r' = Right Red
color 'p' = Right Pink
color 'g' = Right Green
color x   = Left (NotRPG x)

shade :: Char -> Either ShadeError Shade
shade 'f' = Right Full
shade 's' = Right Half
shade 'o' = Right Empty
shade x   = Left (NotFSO x)

card :: String -> Either SetError Card
card [shp, n, c, shd] = Card <$> shape shp <*> number n <*> color c <*> shade shd
card x = Left (NotCard x)

data CardError = NotCard String
instance Show CardError where
    show (NotCard x) = "Card must be 4 characters long, not "    show x

data SetError = CardError | ShapeError | NumberError | ColorError | ShadeError deriving Eq

data ShapeError = NotOSD Char
instance Show ShapeError where
    show (NotOSD x) = "Shape must be o, s, or d, not "    show x

data NumberError = Not123 Char
instance Show NumberError where
    show (Not123 x) = "Number must be 1, 2, or 3, not "    show x

data ColorError = NotRPG Char
instance Show ColorError where
    show (NotRPG x) = "Color must be r, p, or g, not "    show x

data ShadeError = NotFSO Char
instance Show ShadeError where
    show (NotFSO x) = "Shade must be f, s, or o, not "    show x

Current Error

match.hs:84:34:
    Couldn't match type ‘ShapeError’ with ‘SetError’
    Expected type: Either SetError Shape
      Actual type: Either ShapeError Shape
    In the second argument of ‘(<$>)’, namely ‘shape shp’
    In the first argument of ‘(<*>)’, namely ‘Card <$> shape shp’

match.hs:84:48:
    Couldn't match type ‘NumberError’ with ‘SetError’
    Expected type: Either SetError Number
      Actual type: Either NumberError Number
    In the second argument of ‘(<*>)’, namely ‘number n’
    In the first argument of ‘(<*>)’, namely
      ‘Card <$> shape shp <*> number n’

match.hs:84:61:
    Couldn't match type ‘ColorError’ with ‘SetError’
    Expected type: Either SetError Color
      Actual type: Either ColorError Color
    In the second argument of ‘(<*>)’, namely ‘color c’
    In the first argument of ‘(<*>)’, namely
      ‘Card <$> shape shp <*> number n <*> color c’

match.hs:84:73:
    Couldn't match type ‘ShadeError’ with ‘SetError’
    Expected type: Either SetError Shade
      Actual type: Either ShadeError Shade
    In the second argument of ‘(<*>)’, namely ‘shade shd’
    In the expression:
      Card <$> shape shp <*> number n <*> color c <*> shade shd

match.hs:85:16:
    Couldn't match expected type ‘SetError’
                with actual type ‘CardError’
    In the first argument of ‘Left’, namely ‘(NotCard x)’
    In the expression: Left (NotCard x)
Failed, modules loaded: none.

CodePudding user response:

When writing Haskell, don't be afraid of defining lots of types and lots of small functions. Small functions are easier to understand, and can be combined in various ways to define larger functions.

Do you care about the typeclasses...

To get your immediately question out of the way, the Show instance is only used for producing a String from some value. Eq is used to allow comparing two values of the same type with ==. The other classes you can derive instances for (without GHC-specific extensions) are

  • Read
  • Ord
  • Enum
  • Bounded

Read is like the inverse of Show, but is generally not used. (The parsers I show below are easier to write than Read instances, and you won't be able to make use of derived Read instances for most of the types you'll define.) The other three may be relevant to other parts of your program, but not to any of the code in your question.

Derived Show instances basically just convert the data constructor name to a string. Derived Eq instances just check if the two data constructors are the same or not. Definitions for data constructors that take arguments are derived recursively: show (Card x) would be defined using "Card" and show x, while Card c1 == Card c2 would be define as c1 == c2.

Most of your question, though, is about pattern matching (as mentioned in the comments), which has nothing to do with type classes.

...or the types?

Let's start with your four basic concepts: shape, number, color, and shade. Define a separate type for each of them. Here's the type for Shape as an example:

data Shape = ShapeO | ShapeS | ShapeD deriving Eq
instance Show Shape where
    show ShapeO = "O"
    show ShapeS = "S"
    show ShapeD = "D"

Use descriptive names for data constructors where possible (is O really short for Oval? If so, use Oval), and keep in mind that the names must be globally unique (within a module, anyway), so you can't us O for both Shape and Shade. Sometimes the strings derived from the constructor names will be sufficient for your Show instance sometimes not.

Whether you deriving the Show instance or write it manually probably depends on the data constructor names you choose. The derived Eq instances will almost certainly be sufficient, as they do not depend on the names you use for constructor.

(As an aside, Number is tricky because you can't use 1, 2, or 3 as data constructor names. Unless you need to do math with the numbers, you might consider something like data Number = One | Two | Three instead of the overly broad data Number = Number Int.)

Now your Card type can a simple composition of your four attribute types, not just a wrapper around the raw string.

data Card = Card Shape Number Color Shade deriving (Show, Eq)

Parsing: strings to values

You'll want functions that parse a character into an appropriate value, not just check if it could represent an appropriate value. Sometimes this is possible ('o' for ShapeO), sometimes it is not ('x' does not represent a Shape. So your parser should account for this, but not by simply raising an exception with error. Instead, let the return type tell the caller what happened, and let them decide whether the program needs to be terminated. For example,

shape :: Char -> Either String Shape
shape 'o' = Right ShapeO
shape 's' = Right ShapeS
shape 'd' = Right ShapeD
shape x = Left $ "Shape must be o, s, or d, got    show x

The fact that shape returns a Right or Left value is often enough information for the caller to decide what to do next; the error message wrapped by Left is more for reporting errors to the user.

Once you have all the parsers for the individual attributes, the parser for a Card becomes almost trivial, thanks to the Applicative instance for Either String types.

card :: String -> Either String Card
card [shp, n, c, shd] = Card <$> shape shp <*> number n <*> color c <*> shade shd
card x = Left $ "Card must e 4 characters long, got "    show x

If any of the attributes produce a Left value, card will return the Left value of the first attribute that fails to parse. Only when the input string has exactly 4 of the correct characters will char produce a Card value wrapped with Right.

You might decide to define dedicated error types as well, rather than using arbitrary strings. For example,

data ShapeError = NotOSD Char
instance Show ShapeError where
    show (NotOSD x) = "Shape must be o, s, or d, not "    show x

then

shape :: String -> Either ShapeError Shape
shape 'o' = Right ShapeO
shape 's' = Right ShapeS
shape 'd' = Right ShapeD
shape x = Left (NotOSD x)

Basically, capture the information about the error in the error type, and let its Show instance produce a human-readable string with that information if needed.

Things are equal if their parts are equal

match is similarly trivial, making use of the Eq instance derived for Card, which is derived from the Eq instances for all the attribute types.

match :: Card -> Card -> Card -> Bool
match c1 c2 c3 = c1 == c2 && c2 == c3 || (c1 /= c2 && c2 /= c3 && c3 /= c1)
  • Related