Skip to main content

TTC Parse Error Helper Functions

I changed the TTC (Textual Type Classes) Parse API to use Textual error messages long ago. With that change, library users were no longer forced to use the String data type for error messages, but it made parse implementations a bit more verbose. I have been thinking about adding some helper functions for some common cases. Today, I was working on an experimental project that uses TTC, and I decided to go ahead and add those helper functions.

Please note that these changes have been pushed to the develop branch but have not been released yet. I plan on including the changes in the next release (version 1.2.0.0), which I will likely work on next week.

withError

The withError helper function provides a convenient way to return the same error message for any parse error. It is implemented as follows.

withError
  :: (Textual e', Textual e)
  => e'
  -> Maybe a
  -> Either e a
withError err = maybe (Left $ convert err) Right

In this function, e' is the type of the error message in the parse implementation, e is the type of the error message where parse is called, and a is the type of the parsed value. Functions with type suffixes (withErrorS, …) call this function with concrete e' types.

For example, the following Currency implementation uses a Parse instance as a smart constructor to ensure that all Currency values are valid. The withErrorS helper function returns the same error message for any validation error.

... -- language pragmas not shown

module Currency (Currency) where

... -- imports not shown

-- | Currency identifier represented by ISO 4217 alpha code
--
-- The code must be three uppercase ASCII letters.
--
-- Examples:
--
-- * @USD@
-- * @JPY@
--
-- Reference:
--
-- * <https://en.wikipedia.org/wiki/ISO_4217>
newtype Currency = Currency { unCurrency :: Text }
  deriving newtype (Eq, Ord)
  deriving stock   (Show)

instance IsString Currency where
  fromString = TTC.parseUnsafe

instance TTC.Parse Currency where
  parse = TTC.asT $ \t -> TTC.withErrorS "invalid currency code" $ do
    guard $ T.compareLength t 3 == EQ
    guard $ T.all isAsciiUpper t
    pure $ Currency t

instance TTC.Render Currency where
  render = TTC.fromT . unCurrency

prefixError

The prefixError helper function provides a convenient way to add a common prefix to any parse error. It is implemented as follows.

prefixError
  :: (Monoid e', Textual e', Textual e)
  => e'
  -> Either e' a
  -> Either e a
prefixError prefix = either (Left . convert . mappend prefix) Right

Like the withError function, e' is the type of the error message in the parse implementation, e is the type of the error message where parse is called, and a is the type of the parsed value. Functions with type suffixes (prefixErrorS, …) call this function with concrete e' types.

The Currency implementation can be changed to use the prefixErrorS function and provide more error detail.

... -- language pragmas not shown

module Currency (Currency) where

... -- imports not shown

-- | Currency identifier represented by ISO 4217 alpha code
--
-- The code must be three uppercase ASCII letters.
--
-- Examples:
--
-- * @USD@
-- * @JPY@
--
-- Reference:
--
-- * <https://en.wikipedia.org/wiki/ISO_4217>
newtype Currency = Currency { unCurrency :: Text }
  deriving newtype (Eq, Ord)
  deriving stock   (Show)

instance IsString Currency where
  fromString = TTC.parseUnsafe

instance TTC.Parse Currency where
  parse = TTC.asT $ \t -> TTC.prefixErrorS "invalid currency code: " $ do
    unless (T.compareLength t 3 == EQ) $ Left "not three characters long"
    unless (T.all isAsciiUpper t) $ Left "not all uppercase ASCII"
    pure $ Currency t

instance TTC.Render Currency where
  render = TTC.fromT . unCurrency

Notice how the prefixErrorS function makes the type of the error prefix as well as all error messages in the do body String. No calls to convert or from conversion functions are required in the implementation of parse.

Author

Travis Cardwell

Published

Tags
Related Blog Entries