Converters in Haskell

2022-07-03

Summary

In this article, we explore options for converting from one type to another in Haskell. We suggest using functions to do this work, unless converting to/from multiple types - in that case, we suggest using typeclasses, or the Convertible typeclass if you expect to need many converters in your project and can take on extra dependencies, and you're disciplined enough to not abuse its power.

The Problem

I've been working through some Project Euler problems lately. Many of the early problems deal with digits in a number; for example, finding palindromic numbers.

So, in order to help, I created a simple type for dealing with digits:

newtype Digit = Digit Natural

Natural is nice here - digits in a number are always positve - but Natural is way too big for each digit 1..9. So I created a smart constructor to parse digits:

toDigit :: Natural -> Either String Digit
toDigit n
  | n >= 0 && n <= 9 = Right $ Digit n
  | otherwise = Left $ "Can't parse " <> show n <> " to a digit!"

This is fine and dandy when creating Digits from Ints, but it's also common to create Digits from Chars - for example, when turning a Natural into a [Digit]:

digits :: Natural -> Either String [Digit]
digits = mapM toDigit . show

The problem here being that toDigit :: Char -> Either String Digit is not defined. I could map again using some Char -> Int here, but it seems more general (and safer, using Either to catch failures) to use toDigit here. So, I need a way to convert Natural to Digit, and Char to Digit.

Prior work

It's quite common to convert from one type to another. Usually the language you're using will support many conversions to and from builtin types, but it's common to convert to/from types you define.

Regular old functions

How about we just write a function to convert one type to another? This is how I originally approached this, with toDigit.

Pros:

Cons:

Using typeclasses

How about using a typeclass for each of these? eg.

newtype MyNumber = MyNumber Int

class ToDigit a where
  toDigit :: a -> Digit

class PartialToDigit a where
  partialToDigit :: a -> Either String Digit

class FromDigit a where
  fromDigit :: Digit -> a

class PartialFromDigit a where
  partialFromDigit :: Digit -> Either String a

Pros:

Cons:

Using the convertible package

This package declares the Convertible typeclass, for converting from a to b:

type ConvertResult a = Either ConvertError a

class Convertible a b where
  safeConvert :: a -> ConvertResult b

There's also a handy convert function, but it's partial:

convert :: Convertible a b => a -> b
convert x =
    case safeConvert x of
      Left e -> error (prettyConvertError e)
      Right r -> r

Similarities to Scala's implicit conversions

This Convertible typeclass reminds me of Scala's implicit conversions in some ways: you can convert between two types by simply using the convert keyword, as long as there's a way to convert between the two. In this way, you can make a function take just about any argument as long as it converts to the type that you need.

For example, you could implement some sort of boolean coercion using Convertible:

instance Convertible Int Bool where
  safeConvert n = Right $ n /= 0

-- | Prints "bar".
main :: IO ()
main =
  if convert 0
  then putStrLn "foo"
  else putStrLn "bar"

Pros:

Cons:

Thanks for reading!

Feel free to reach out if you have any comments to:
sixstring982 (at) gmail (dot) com.