Key phone in Haskell

Posted on

Problem

Here is my solution for an exercise, which requires translation of strings to key presses on a phone keypad (for example, to get ‘b’ the digit ‘2’ needs to be pressed twice).

It’s taken from the book Haskell from first principles, and originally appeared in 1HaskellADay. I’d love to get feedback.

module Phone where
import Data.Char
import Data.String.Utils
import Data.List

data DaPhone = DaPhone [String]
convo :: [String]
convo =
          ["Wanna play 20 questions",
           "Ya",
           "U 1st haha",
           "Lol ok. Have u ever tasted alcohol lol",
           "Lol ya",
           "Wow ur cool haha. Ur turn",
           "Ok. Do u think I am pretty Lol",
           "Lol ya",
           "Haha thanks just making sure rofl ur turn"]

-- validButtons = "1234567890*#"
type Digit = Char
-- Valid presses: 1 and up
type Presses = Int

reverseTaps :: DaPhone -> Char -> [(Digit, Presses)]
reverseTaps (DaPhone keys) c = if isUpper c then ('*', 1) : looks else looks
    where looks = look keys (toLower c)
-- assuming the default phone definition
-- 'a' -> [('2', 1)]
-- 'A' -> [('*', 1), ('2', 1)]

look :: [String] -> Char -> [(Digit, Presses)]
look keys c = look' keys c 0

look' :: [String] -> Char -> Int -> [(Digit, Presses)]
look' [] _ _ = []
look' (x:xs) c n = if ind /= (-1) then [(head $ show n, ind + 1)] else look' xs c (n + 1)
where ind = maybe (-1) id $ elemIndex c x

cellPhonesDead :: DaPhone -> String -> [(Digit, Presses)]
cellPhonesDead = (concat .) . map . reverseTaps

-- count total presses
fingerTaps :: [(Digit, Presses)] -> Presses
fingerTaps = sum . map snd

mostPopularLetter :: String -> Char
mostPopularLetter = head . longest . group . sort

coolestLtr :: [String] -> Char
coolestLtr = mostPopularLetter . filter isAlpha . concat

coolestWord :: [String] -> String
coolestWord = head . longest . group . sort . words . join " "

longest :: [[a]] -> [a]
longest = maximumBy (x y -> compare (length x) (length y))

phone :: DaPhone
phone = DaPhone keymap

keymap :: [String]
keymap =

                [
                    " 0",
                    "1",
                    "abc2",
                    "def3",
                    "ghi4",
                    "jkl5",
                    "mno6",
                    "pqrs7",
                    "tuv8",
                    "wxyz9"
                ]

Solution

In real code, I would consider this to be too much naming for this little code. Most of this would probably be used only once and could thus be inlined. Since the exercise required implementations for all these, it’s okay.

(concat .) . map is foldMap.

x y -> compare (length x) (length y) is comparing length.

Did you try compiling this? ind /= Nothing means that ind + 1 shouldn’t work, and elemIndex takes the Char before the String.

look keys c = case asum $ zipWith (fmap . (,)) [0..] $ map (elemIndex c) keys of
  Nothing -> []
  Just (n, ind) -> [(head $ show n, ind + 1)]

I am wondering whether DaPhone could have a better data structure, because I think your passing number around makes code complex. Such as:

data DaPhone = DaPhone [(Char, String)]

reverseTaps :: DaPhone -> Char -> [(Digit, Presses)]
reverseTaps phone c =
     if isUpper c
     then [('*', 1), look phone (toLower c)]
     else [look phone c]

look :: DaPhone -> Char -> (Digit, Presses)
look (DaPhone ((digit, press):tl)) c =
     case elemIndex c press of
       Just idx -> (digit, idx + 1)
       Nothing -> look (DaPhone tl) c

cellPhonesDead :: DaPhone -> String -> [(Digit, Presses)]
cellPhonesDead phone = foldMap (reverseTaps phone)

What I assume:

  • look will not fail.
  • DaPhone will store like (‘2’, ‘abc2’) instead of (‘2’, ‘abc’).

Leave a Reply

Your email address will not be published. Required fields are marked *