packages feed

parsable-0.1.0.0: src/Data/Parsable.hs

{-|
Module      : Data.Parsable

This module contains two simple classes, 'Parsable' and 'Printable'.

There is an implicit "soft isomorphism" between 'parser' and 'toString'.
(Successfully parsing a string and then running 'toString' on the result
should result in the original string.)

=== Language extensions

Because 'parser' does not take any arguments, it may be necessary
to explicitly declare the type of @t@ for these functions.

It may be helpful to enable and use the @TypeApplications@ and possibly
@ScopedTypeVariables@ extensions..

Look at the @Language Extensions@ section of the GHC documentation for
instructions on how to use these extensions.
-}

{-# Language DefaultSignatures #-}
{-# Language DeriveDataTypeable #-}
{-# Language DeriveGeneric #-}
{-# Language DeriveTraversable #-}
{-# Language DerivingVia #-}
{-# Language FlexibleContexts #-}
{-# Language FlexibleInstances #-}
{-# Language GeneralizedNewtypeDeriving #-}
{-# Language MultiParamTypeClasses #-}
{-# Language OverloadedStrings #-}
{-# Language QuantifiedConstraints #-}
{-# Language ScopedTypeVariables #-}
{-# Language TypeApplications #-}
{-# Language TypeFamilies #-}
{-# Language UndecidableInstances #-}

module Data.Parsable
    (
    -- * Parsing
      Parsable(..)
    , ParserName(..)
    , runParsableT
    , runParsable
    -- ** Wrappers
    , NaturalParsable(..)
    -- * Parsing functions
    , satisfyAny
    , wordAllowed
    , readParsec
    -- * Printing
    , Printable(..)
    , toText
    -- ** Wrappers
    , ShowPrintable(..)
    -- * Re-exports
    , module Control.Monad
    , module Control.Monad.Trans.Class
    , module Data.Char
    , module Data.Functor.Identity
    , module Data.String
    , module Text.Parsec
    , module Text.Parsec.Char
    ) where

import Control.Applicative hiding (many)
import Control.Monad
import Control.Monad.Trans.Class
import Data.Char
import Data.Data
import Data.Functor.Identity
import Data.Kind
import Data.String
import Data.Text (Text, unpack)
import GHC.Generics
import Text.Parsec
import Text.Parsec.Char
import Text.Read (readMaybe)

newtype ParserName a s u (m :: Type -> Type) = ParserName { getParserName :: String }
    deriving stock (Show, Eq, Ord)
    deriving newtype IsString

-- | Represents types that have a valid Parsec parser.
class Parsable a (m :: Type -> Type) u s where
    parser :: ParsecT s u m a
    parserName :: ParserName a s u m
    {-# Minimal parser, parserName #-}

newtype NaturalParsable a = NaturalParsable
    { unwrapNaturalParsable :: a }

instance (Stream s m Char, Read a, Typeable a)
    => Parsable (NaturalParsable a) m u s where
    parserName = "natural number"
    parser = (<?> "natural number") $ do
        ds <- some (satisfy isDigit)
        case readMaybe ds of
            Just i -> pure $ NaturalParsable i
            Nothing -> fail $ "Could not parse as " ++ t ++ ": " ++ show ds
      where
        t = show $ typeRep $ Proxy @a

-- | Convenience function to run a 'Parsable' parser.
runParsableT :: forall a m s. (Stream s m Char, Parsable a m () s)
    => String -> s -> m (Either ParseError a)
runParsableT = runParserT (parser <?> n) ()
    where n = getParserName $ parserName @a @m @() @s

runParsable :: forall a s. (Stream s Identity Char, Parsable a Identity () s)
    => String -> s -> Either ParseError a
runParsable = runParser (parser <?> n) ()
    where n = getParserName $ parserName @a @Identity @() @s

-- | Pass a previously-parsed string to this function in order to attempt
--   using 'read'. Produces proper error messages on failure.
readParsec :: forall a s u m. (Typeable a, Read a) => String -> ParsecT s u m a
readParsec s = (<?> typeName ++ " (Read instance)") $
    case readMaybe s of
        Just x -> pure x
        Nothing -> fail
            $  "unable to parse using "
            ++ typeName
            ++ " Read instance: "
            ++ show s
    where typeName = show $ typeRep $ Proxy @a

-- | Parse a token that satisfies any of the given predicates
satisfyAny :: Stream s m Char => [Char -> Bool] -> ParsecT s u m Char
satisfyAny fs = satisfy $ \c -> or [f c | f <- fs]

-- | Parsing of "words" which require a list of predicates for the first
--   token, and a list of predicates for any remaining tokens. This always
--   parses at least one token.
wordAllowed :: Stream s m Char
    => [Char -> Bool] -- ^ Tokens that start the word
    -> [Char -> Bool] -- ^ Any subsequent tokens
    -> ParsecT s u m [Char]
wordAllowed beg rest = (:) <$> satisfyAny beg <*> many (satisfyAny rest)

-- | Types that can be converted back to a @String@.
class Printable t where
    toString :: t -> String

    default toString :: Show t => t -> String
    toString = show
    {-# Minimal toString #-}

instance Printable String where
    toString = id

instance Printable Text where
    toString = unpack

-- | Convenience function that will turn a 'Printable' to any 'IsString'.
toText :: (Printable t, IsString s) => t -> s
toText = fromString . toString

-- | Wrapper for types that inherit 'toString' directly from their 'Show' instance.
--
--   It is convenient to use the @DerivingVia@ language extension with this.
--
--   > {-# Language DerivingVia #-}
--   >
--   > newtype MyNum Int
--   >     deriving Printable via (ShowPrintable Int) -- Uses Show instance of Int
newtype ShowPrintable a = ShowPrintable
    { unwrapShowPrintable :: a }
    deriving stock
        ( Read, Show, Eq, Ord, Bounded, Functor, Foldable, Traversable, Generic
        , Generic1, Data
        )
    deriving newtype (Enum, Num, Fractional, Floating, IsString, Semigroup, Monoid)
    deriving (Applicative, Monad) via Identity

-- | Uses 'show' after unwrapping the contents.
instance Show a => Printable (ShowPrintable a) where
    toString = show . unwrapShowPrintable