Inverting functions

Often we need a pair of conversion functions: one to encode a value as a string, and another corresponding function to decode a string back into the original type. Here we show a concise way to define these functions for datatypes whose constructors enumerate a small fixed set of possibilities (sometimes described as “enums”).

We will make use of a few language extensions (GHC generics and the anyclass strategy) to let the compiler automatically generate a list of all the values of an datatype.

{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveAnyClass #-}

We use the deriving strategies extension to be explicit about which deriving mechanism is in use for each typeclass.

{-# LANGUAGE DerivingStrategies #-}

This code uses \case expressions.

{-# LANGUAGE LambdaCase #-}

In addition to GHC’s Generic class, we’ll also need to import the GEnum class from the generic-deriving library. When a type has an instance of the GEnum class, we can use genum to obtain a list of all values of that type.

import GHC.Generics (Generic)
import Generics.Deriving.Enum (GEnum (genum))

We’ll be using the Map data structure from the containers library.

import qualified Data.Map.Strict as Map

First we’ll define two enum datatypes to use for examples. Let’s imagine we’re writing code related to billing for a subscription service. The product comes in three varieties – basic, standard, and pro – and our users can choose to pay either monthly or annually.

For each type, we derive instances of the Generic and GEnum classes. We will not be directly using the Generic class, but it is a requirement for deriving the GEnum class which we will be using.

data Product = Basic | Standard | Pro
    deriving stock (Generic, Show)
    deriving anyclass GEnum

data Frequency = Monthly | Annual
    deriving stock (Generic, Show)
    deriving anyclass GEnum

We’ll also define a Bill datatype, consisting of a product and a billing frequency. These two fields represent a summary of a subscriber’s invoice.

data Bill = Bill Product Frequency
    deriving stock (Generic, Show)
    deriving anyclass GEnum

Here are some example encoding functions.

For our first example, we define a way to represent values of the Product type as Strings.

encodeProduct :: Product -> String
encodeProduct = \case
    Basic    -> "p1"
    Standard -> "p2"
    Pro      -> "p3"

Next we define a way to encode the Bill type as an Integer. Since a bill consists of a Product (of which there are three) and a Frequency (of which there are two), then there are 3 × 2 = 6 values of Bill to encode.

Decoding is converting back in the other direction – for example, Integer -> Maybe Bill. The return type includes Maybe because not every integer represents a bill, so decoding can fail.

It could be rather tedious and redundant to also write to corresponding decoding functions. Fortunately, we can take a shortcut.

encodeBill :: Bill -> Integer
encodeBill = \case
    Bill Basic    Monthly -> 10
    Bill Basic    Annual  -> 11
    Bill Standard Monthly -> 20
    Bill Standard Annual  -> 21
    Bill Pro      Monthly -> 30
    Bill Pro      Annual  -> 31

If the type a we’re encoding has an instance of GEnum and we have an encoding function f :: a -> b such as encodeProduct or encodeBill, then this invert function will infer the corresponding decoding function.

invert :: (GEnum a, Ord b) => (a -> b) -> b -> Maybe a

The first step is to construct reverseMap, a Map that contains, for every value of type a, a mapping from the encoded form back to the original value. This is where we use genum to list all values of type a.

The b -> Maybe a decoding function, then, consists of a lookup from that map.

invert f =
    let
        reverseMap = foldMap (\a -> Map.singleton (f a) a) genum
    in
        \b -> Map.lookup b reverseMap

Now we can use the invert function to generate the decoding functions for Product and Bill with minimal effort.

decodeProduct :: String -> Maybe Product
decodeProduct = invert encodeProduct

decodeBill :: Integer -> Maybe Bill
decodeBill = invert encodeBill
main =
  do

We demonstrate by printing the results of some encodings and decodings.

    putStrLn (encodeProduct Basic)
    putStrLn (encodeProduct Standard)

    putStrLn (show (decodeProduct "p1"))
    putStrLn (show (decodeProduct "xyz"))

    putStrLn (show (encodeBill (Bill Basic Annual)))
    putStrLn (show (encodeBill (Bill Pro Monthly)))

    putStrLn (show (decodeBill 31))
    putStrLn (show (decodeBill 50))

encodeProduct maps Basic to "p1" and Standard to "p2".

decodeProduct maps "p1" back to Just Basic. But it maps "xyz" to Nothing, because "xyz" is not the encoding of any product.

$ runhaskell invert.hs
p1
p2
Just Basic
Nothing
11
30
Just (Bill Pro Annual)
Nothing