Frames-streamly-0.3.1.0: src/Frames/Streamly/ColumnTypeable.hs
{-# LANGUAGE AllowAmbiguousTypes, BangPatterns,
DefaultSignatures, DeriveFunctor,
FlexibleInstances,
LambdaCase,
MultiParamTypeClasses, RankNTypes, ScopedTypeVariables
, TemplateHaskell, TypeFamilies #-}
module Frames.Streamly.ColumnTypeable where
import Prelude hiding (Const(..), Type)
import Frames.Streamly.OrMissing
import Data.Readable (Readable(fromText))
import Data.Typeable (typeRep)
import qualified Data.Text as T
import Data.Vinyl.Functor (Const(..))
import Language.Haskell.TH
-- | Type to represent the result of successfully parsing a @Text@ to @a@.
-- Usually will be @Definitely@ but @Possibly@ may be used if further
-- parsing to type @a@ may show that @a@ was not possible type for the
-- entire column. Used for the @Categorical@ type.
data Parsed a = Possibly a | Definitely a deriving (Eq, Ord, Show, Functor)
-- | Values that can be read from a 'T.Text' with more or less
-- discrimination.
class Parseable a where
-- | E.g., if @m@ is 'Maybe',
-- Returns 'Nothing' if a value of the given type can not be read;
-- returns 'Just Possibly' if a value can be read, but is likely
-- ambiguous (e.g. an empty string); returns 'Just Definitely' if a
-- value can be read and is unlikely to be ambiguous.
parse :: MonadPlus m => T.Text -> m (Parsed a)
default parse :: (Readable a, MonadPlus m)
=> T.Text -> m (Parsed a)
parse = fmap Definitely . fromText
{-# INLINE parse #-}
-- | Combine two parse results such that the combination can
-- fail. Useful when we have two 'Possibly' parsed values that are
-- different enough to suggest the parse of each should be
-- considered a failure. The default implementation is to 'return'
-- the first argument.
parseCombine :: MonadPlus m => Parsed a -> Parsed a -> m (Parsed a)
default parseCombine :: MonadPlus m => Parsed a -> Parsed a -> m (Parsed a)
parseCombine = const . return
{-# INLINE parseCombine #-}
representableAsType :: Parsed a -> Const (Either (String -> Q [Dec]) Type) a
default
representableAsType :: Typeable a
=> Parsed a -> Const (Either (String -> Q [Dec]) Type) a
representableAsType =
const (Const (Right (ConT (mkName (show (typeRep (Proxy :: Proxy a)))))))
{-# INLINABLE representableAsType #-}
-- | Discard any estimate of a parse's ambiguity.
discardConfidence :: Parsed a -> a
discardConfidence (Possibly x) = x
discardConfidence (Definitely x) = x
{-# INLINE discardConfidence #-}
-- | Acts just like 'fromText': tries to parse a value from a 'T.Text'
-- and discards any estimate of the parse's ambiguity.
parse' :: (MonadPlus m, Parseable a) => T.Text -> m a
parse' = fmap discardConfidence . parse
{-# INLINE parse' #-}
parseIntish :: (Readable a, MonadPlus f) => T.Text -> f (Parsed a)
parseIntish t =
Definitely <$> fromText (fromMaybe t (T.stripSuffix (T.pack ".0") t))
{-# INLINEABLE parseIntish #-}
instance Parseable Bool where
instance Parseable Int where
parse = parseIntish
{-# INLINE parse #-}
instance Parseable Int32 where
parse = parseIntish
{-# INLINE parse #-}
instance Parseable Int64 where
parse = parseIntish
{-# INLINE parse #-}
instance Parseable Integer where
parse = parseIntish
{-# INLINE parse #-}
instance Parseable Float where
instance Parseable Double where
-- Some CSV's export Doubles in a format like '1,000.00', filtering
-- out commas lets us parse those sucessfully
parse = fmap Definitely . fromText . T.filter (/= ',')
{-# INLINE parse #-}
instance Parseable T.Text where
-- @adamConnerSax new/changed stuff
-- | This class relates a universe of possible column types to Haskell
-- types, and provides a mechanism to infer which type best represents
-- some textual data.
-- The type @a@ must somehow represent the list of all possible inferred types and
-- which of those are currently possible given the data seen so far.
class ColumnTypeable a where
type ParseType a
-- ^ An associated type to represent the result of parsing a single @Text@ into possible column types
-- represented by @a@
type Parsers a
-- ^ An associated type to represent information required to parse @Text@ to
-- possible types all held in @a@, combine the current slate of possible types and
-- a newly parsed set of possibilities and produce template haskell to declare the
-- "best" (most specific) type in @a@.
colType :: Parsers a -> a -> Either (String -> Q [Dec]) Type
-- ^ TH haskell representing the best type among those possible in @a@.
-- Used when declaring the type after inference.
inferType :: Parsers a -> (T.Text -> Bool) -> T.Text -> ParseType a
-- ^ Given parsing info and a function to indicate "missing" data, parse a @Text@ to possible types @a@
initialColType :: a
-- ^ A value of type @a@ to be used as the initial state in iterative column type inference.
-- Usually indicates all types are still possible.
updateWithParse :: Parsers a -> a -> ParseType a -> a
-- ^ given parsing info, an @a@ representing possible column types given the data so far,
-- and a parsed value, indicating possible types of the current item of data, update the
-- possible types for the column.
instance (Typeable a, Parseable a) => Parseable (OrMissing a) where
parse t = return $ maybe (Definitely Missing) (fmap Present) $ parse t
{-# INLINEABLE parse #-}
parseCombine p1 p2 = case (commute p1, commute p2) of
(Missing, Missing) -> return $ Definitely Missing
(Present x, Missing) -> return $ fmap Present x
(Missing, Present x) -> return $ fmap Present x
(Present x, Present y) -> fmap Present <$> parseCombine x y
where
commute :: Parsed (OrMissing a) -> OrMissing (Parsed a)
commute (Possibly Missing) = Missing
commute (Possibly (Present a)) = Present (Possibly a)
commute (Definitely Missing) = Missing
commute (Definitely (Present a)) = Present (Definitely a)
{-# INLINEABLE parseCombine #-}
-- | Record-of-functions for column parsing.
data ParseHow a = ParseHow
{ phParse :: forall m. MonadPlus m => T.Text -> m (Parsed a)
-- ^ attempt to parse the given 'Text' into the type @a@
, phParseCombine :: forall m. MonadPlus m => Parsed a -> Parsed a -> m (Parsed a)
-- ^ check that two parsed values of type a are both consistent with being of type @a@
, phRepresentableAsType :: Parsed a -> Either (String -> Q [Dec]) Type
-- ^ Template-Haskell for declaring type @a@. Either a function to produce a set of
-- declarations (for 'Categorical') or the template-haskell 'Type' which can easily be
-- turned into a declaration.
}
-- | Generate a 'ParseHow' for a any type with a 'Parseable' instance.
parseableParseHow :: Parseable a => ParseHow a
parseableParseHow = ParseHow parse parseCombine (getConst . representableAsType)
{-# INLINEABLE parseableParseHow #-}
-- | Generate a 'ParseHow' for any type with a 'Typeable' instance and a parsing function of
-- the form @Text -> Maybe a@.
simpleParseHow :: forall a . Typeable a => (Text -> Maybe a) -> ParseHow a
simpleParseHow g = ParseHow p c r where
p :: forall n. MonadPlus n => T.Text -> n (Parsed a) -- this sig required for MonadPlus constraint
p = maybe mzero (return . Definitely) . g
c pa _ = return pa
r _ = Right (ConT (mkName (show (typeRep (Proxy :: Proxy a)))))
{-# INLINEABLE simpleParseHow #-}