简体   繁体   中英

Haskell custom data type and reprsentation

Say there is a stadium and the row number is something like A1-10, then B1-10 and so on until ZZ

How do I make a custom data type and use it to represent the seat in Haskell?

You can think of your enumeration as being composed of three parts

  • a first letter,
  • an (optional) second letter, and
  • a number between 1 and 10

The first and second part both rely upon the notion of "letter", so let's define that

data Letter
  = La | Lb
  | Lc | Ld
  | Le | Lf
  | Lg | Lh
  | Li | Lj
  | Lk | Ll
  | Lm | Ln
  | Lo | Lp
  | Lq | Lr
  | Ls | Lt
  | Lu | Lv
  | Lw | Lx
  | Ly | Lz
  deriving ( Eq, Ord, Show )

This type is explicitly enumerated instead of merely using Char so that we don't have to worry about the differences between lower and upper case or the problems of Char containing extra things like '-' and '^' . Since I enumerated the elements in alphabetical order, the autoderived instances like Ord behave properly.

We probably do want to take advantage of the fact that Letter is a subset of Char so let's write the projections, too.

-- This one always works since every letter is a character.

letterToChar :: Letter -> Char
letterToChar l = case l of
  La -> 'a'
  Lb -> 'b'
  Lc -> 'c'
  Ld -> 'd'
  Le -> 'e'
  Lf -> 'f'
  Lg -> 'g'
  Lh -> 'h'
  Li -> 'i'
  Lj -> 'j'
  Lk -> 'k'
  Ll -> 'l'
  Lm -> 'm'
  Ln -> 'n'
  Lo -> 'o'
  Lp -> 'p'
  Lq -> 'q'
  Lr -> 'r'
  Ls -> 's'
  Lt -> 't'
  Lu -> 'u'
  Lv -> 'v'
  Lw -> 'w'
  Lx -> 'x'
  Ly -> 'y'
  Lz -> 'z'


-- This one might fail since some characters aren't letters. We also do
-- automatic case compensation.
charToLetter :: Char -> Maybe Letter
charToLetter c = case Char.toLower of
  'a' -> Just La
  'b' -> Just Lb
  'c' -> Just Lc
  'd' -> Just Ld
  'e' -> Just Le
  'f' -> Just Lf
  'g' -> Just Lg
  'h' -> Just Lh
  'i' -> Just Li
  'j' -> Just Lj
  'k' -> Just Lk
  'l' -> Just Ll
  'm' -> Just Lm
  'n' -> Just Ln
  'o' -> Just Lo
  'p' -> Just Lp
  'q' -> Just Lq
  'r' -> Just Lr
  's' -> Just Ls
  't' -> Just Lt
  'u' -> Just Lu
  'v' -> Just Lv
  'w' -> Just Lw
  'x' -> Just Lx
  'y' -> Just Ly
  'z' -> Just Lz
  _   -> Nothing -- default case, no match

Now we play the same game with "digits from 1 to 10"

data Digit
  = D1 | D2
  | D3 | D4
  | ...
  deriving ( Eq, Ord, Show )

digitToInt :: Digit -> Int
digitToInt = ...

intToDigit :: Int -> Maybe Digit
intToDigit = ...

We might even write other ways of retracting an Int to a Digit . For instance, we could (1) take the absolute value of the integer and then (2) take its div and mod against 10 seats. This will result in a Digit assignment and a row number.

intToDigitWrap :: Int -> (Int, Digit)
intToDigitWrap n = (row, dig) where
  (row, dig0) = n `divMod` 10
  -- we use an incomplete pattern match because we have an invariant
  -- now that (dig0 + 1) is in [1, 10] so intToDigit always succeeds
  Just dig    = intToDigit (dig0 + 1) 

And the final type is easy!

data Seat = Seat { letter1 :: Letter
                 , letter2 :: Maybe Letter
                 , digit   :: Digit
                 } deriving ( Eq, Ord, Show )

The Ord type is again completely automatically correct as Nothing is less than Show x for any x and record ordering is lexicographic. We can also write a show instance that's a bit friendlier very simply

prettySeat :: Seat -> String
prettySeat s = 
  let l1 = [Char.toUpper $ letterToChar $ letter1 s]
      l2 = case letter2 s of
             Nothing -> ""
             Just c  -> [Char.toUpper $ letterToChar c]
      dig = show (digitToInt (digit s))
  in l1 ++ l2 ++ "-" ++ dig

In all likelihood the ability to inject the Letter and Digit types into their superset types Char and Int respectively will almost certainly come in handy when writing code later.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM