Skip to content

Instantly share code, notes, and snippets.

@rampion
Last active May 29, 2020 16:40
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save rampion/a24f783b75b16f1fe57a to your computer and use it in GitHub Desktop.
Save rampion/a24f783b75b16f1fe57a to your computer and use it in GitHub Desktop.

So we're given the task of determining whether a given integer is a palindrome in base 10.

Seems easy enough:

isPalindrome :: Integer -> Bool
isPalindrome n = show n == reverse (show n)

Done. That was a short article.

Oh wait, type String = [Char] and we can't use lists.

Well, let's see if we can refactor this into something that doesn't use lists, step by step.


First, let's get rid of that use of show. It made me feel kinda sketchy anyway, using strings.

We can get rid of the one on the left by applying read to both sides:

isPalindrome n = read (show n) == read (reverse (show n))

(To handle negative numbers, and numbers ending in 0, I really should use read' = read . takeWhile isDigit . dropWhile (=='0'), so let's imagine I did).

Then we can use read . show == id (for Integer, at least) to simplify:

isPalindrome n = n == read (reverse (show n))

Since we'll be playing with the RHS of the == for the rest, let's extract that to its own function:

isPalindrome = n == reversal n

reversal :: Integer -> Integer
reversal = read . reverse . show

To get rid of the read and show, let's come up with a way to convert an integer to and from a list of digits:

reversal = fromDigits . reverse . toDigits

type Digit = Integer

toDigits :: Integer -> [Digit]
toDigits = unfoldr getDigit
  where getDigit 0 = Nothing
        getDigit n = let (q,r) = n `quotRem` 10 in Just (r,q)

fromDigits :: [Digit] -> Integer
fromDigits = foldr useDigit 0
  where useDigit d m = 10*m + d

(I should caveat that we've changed the behaviour of reversal a bit. Now reversal (-12345) == -54321 where previously it returned 54321. If you have strong opinions on whether negative numbers can be palindromes, be aware).

Now expanding the definition of toDigits and fromDigits in reversal we get:

reversal = foldr useDigit 0 . reverse . unfoldr getDigit
  where getDigit 0 = Nothing
        getDigit n = let (q,r) = n `quotRem` 10 in Just (r,q)
        useDigit d m = 10*m + d

Now, foldr f b . reverse == foldl (flip f) b, so by swapping the argument order for useDigit, we can simplify this to:

reversal = foldl useDigit 0 . unfoldr getDigit
  where getDigit 0 = Nothing
        getDigit n = let (q,r) = n `quotRem` 10 in Just (r,q)
        useDigit m d = 10*m + d

The thing about foldl f c . unfoldr g is that it's really modeling a linear tail-recursive function. It uses g to unpack a value from the input, which f then combines with c. When g can't unpack any more from the input, it returns c.

We can always refactor foldl f c . unfoldr g into

foldl f c . unfoldr g == go c 
  where go c b = case g b of
          Nothing       -> c
          Just (a, b')  -> go (f c a) b'

Applying this transformation, we get

reversal = go 0
  where getDigit 0 = Nothing
        getDigit n = let (q,r) = n `quotRem` 10 in Just (r,q)
        useDigit m d = 10*m + d
        go c b = case getDigit b of
          Nothing       -> c
          Just (a, b')  -> go (useDigit c a) b'

Now we can simplify, inlining getDigit and useDigit:

reversal = go 0
  where go c 0 = c
        go c n = let (q,r) = n `quotRem` 10 in go (10*c + r) q

Which gives us a pretty elegant looking solution.

Comment and complain on reddit.

@WillNess
Copy link

nice! fusing and inlining can make compilers write nice code too, huh.

you have a typo: reversal = toDigits . reverse . fromDigits should be the other way around.

@rampion
Copy link
Author

rampion commented May 29, 2020

@WillNess Thanks! Fixed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment