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 Digit
s from Int
s, but it's also common
to create Digit
s from Char
s - 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:
- Works fine for single type conversions. This is probably what you should use unless you know that you'll need to convert more than one type.
Cons:
- It'd be nice to be able to convert multiple types in this way - say,
having a
toFoo
function that could take aBar
or aBaz
.
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:
- Simple
- Provides a consistent API: can be adapted for any type using this naming convention
Cons:
- More boilerplate, but can probably be alleviated with some Template Haskell.
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:
- One typeclass to rule them all
- Some nice utilities for failure modes
Cons:
- This technique is pretty powerful, and can be used to make types more "dynamic" than they probably should be. Care should be taken to avoid over-abstraction.
Thanks for reading!
Feel free to reach out if you have any comments to: