packages feed

Frames-streamly-0.3.1.0: src/Frames/Streamly/ColumnUniverse.hs

{-# LANGUAGE AllowAmbiguousTypes, BangPatterns, CPP, ConstraintKinds, DataKinds,
             DerivingVia,
             FlexibleContexts, FlexibleInstances, GADTs, InstanceSigs,
             KindSignatures, LambdaCase, MultiParamTypeClasses,
             OverloadedStrings, QuasiQuotes, RankNTypes, RecordWildCards,
             ScopedTypeVariables, StandaloneKindSignatures,
             TemplateHaskell, TupleSections, TypeApplications,
             TypeFamilies, TypeOperators, UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Frames.Streamly.ColumnUniverse (
  CommonColumns
  , CommonColumnsCat
  , ParseHowRec
  , parseableParseHowRec
  , tryParseAll
  , CanParseAs(..)
  , ParseResult(..)
  , parseResult
  , parseResult'
  , ColType(..)
  , SomeMissing(..)
  , colTypeTH
  , colTypeSomeMissing
  , addParsedCell
  , initialColType
) where

import Prelude hiding (Compose(..), Type, getConst, show, Const(..))
import qualified Data.Text as T
import Data.Vinyl
import Data.Vinyl.Functor
import Frames.Streamly.ColumnTypeable
import Frames.Streamly.Categorical
import Language.Haskell.TH

-- | Use a @ParseHow@ to (possibly) parse a given @Text@ as a value of type @a@
inferParseable :: ParseHow a -> T.Text -> (Maybe :. Parsed) a
inferParseable ParseHow{..} = Compose . phParse
{-# INLINE inferParseable #-}

-- | Helper to call 'inferParseable' on a 'Rec'.
inferParseable' :: ParseHow a -> (((->) T.Text) :. (Maybe :. Parsed)) a
inferParseable'  = Compose . inferParseable
{-# INLINE inferParseable' #-}

-- * Record Helpers

type ParseHowRec ts = Rec ParseHow ts
-- | Generate a Rec of @ParseHow@ for a list of types with 'Parseable' instances.
parseableParseHowRec :: RPureConstrained Parseable ts => ParseHowRec ts
parseableParseHowRec = rpureConstrained @Parseable parseableParseHow
{-# INLINEABLE parseableParseHowRec #-}

-- | Try to parse the given 'Text' to each type in a type-list, putting the result in a Rec.
tryParseAll :: forall ts. (RMap ts)
            => ParseHowRec ts -> T.Text -> Rec (Maybe :. Parsed) ts
tryParseAll phR = rtraverse getCompose funs
  where funs :: Rec (((->) T.Text) :. (Maybe :. Parsed)) ts
        funs = rmap inferParseable' phR
{-# INLINABLE tryParseAll #-}

-- * Common Columns

-- | Common column types: 'Bool', 'Int', 'Double', 'T.Text'
type CommonColumns = [Bool, Int, Double, T.Text]

-- | Common column types including categorical types.
-- NB: If @Categorical 8@ is too small for some types you can
-- increase the number or *add* a larger 'Categorical' after. e.g.,
-- @MoreColumnsCat = [Bool, Int, Double, Categorical 8, Categorical 32, T.Text]@
type CommonColumnsCat = [Bool, Int, Double, Categorical 8, T.Text]

-- | A universe of common column variants. These are the default
-- column types that @Frames@ can infer. See the
-- <http://acowley.github.io/Frames/#sec-4 Tutorial> for an example of
-- extending the default types with your own.
--type Columns = ColumnUniverse CommonColumns

-- @adamConnerSax new stuff
-- | Type to flag whether some data in a column is missing.
data SomeMissing = SomeMissing | NoneMissing deriving (Eq, Show)

-- | Type to hold the result of an attempt to parse at each possible type.
-- Isomorphic to @Maybe (Parsed p)@ but makes the code clearer.
data CanParseAs p = YesParse (Parsed p) | NoParse

-- | Given the 'ParseHow' record-of-functions for @p@, combine two possible
-- parses of @p@ into a possible parse of @p@.  Largely, this amounts to
-- success if both succeeded and failure if either failed.  But for some types,
-- e.g., 'Categorical` two previous parses can succeed but the combination can fail.
-- In the case of `Categorical` that can happen because we discover, for example, a
-- 9th alternative when we are trying to parse as @Categorical 8@. In general, this
-- could be useful anytime the success or failure of parsing a column as type @p@
-- depends on some property of the set of all values present in the column rather
-- than just if each single parse attempt succeeds.
combineCanParseAs :: ParseHow p -> CanParseAs p -> CanParseAs p -> CanParseAs p
combineCanParseAs ParseHow{..} (YesParse x) (YesParse y) = maybe NoParse YesParse $ phParseCombine x y
combineCanParseAs _ _ _ = NoParse

-- | Holder for the results of attempting to parse as all the types
-- in the type-list @ts@.  Isomorphic to @Maybe (Rec CanParseAs ts)@
data ParseResult ts = MissingData | ParseResult (Rec CanParseAs ts)

-- | Given a 'ParseHow' for each type in @ts@ and a function to indicate which data is "missing"
-- attempt to parse a given 'Text' as each type in @ts@ and return the result as possible
-- parses at each type.
parseResult' :: RMap ts => ParseHowRec ts -> (Text -> Bool) -> Text -> ParseResult ts
parseResult' phRec missingF t
  | missingF t = MissingData
  | otherwise = ParseResult $ recParsedToRecCanParseAs $ tryParseAll phRec t
{-# INLINEABLE parseResult' #-}

-- | Given a 'ParseHow' for each type in @ts@,
-- attempt to parse a given 'Text' as each type in @ts@ and return the result as possible
-- parses at each type.
-- NB: Uses a default definition of "missing" data: "" or "NA"
parseResult ::  RMap ts => ParseHowRec ts -> Text -> ParseResult ts
parseResult phR = parseResult' phR defaultMissing where
  defaultMissing t = T.null t || t == "NA"
{-# INLINEABLE parseResult #-}


-- | Helper for converting @Maybe :. Parsed@ to  'CanParseAs'
recParsedToRecCanParseAs :: RMap ts => Rec (Maybe :. Parsed) ts -> Rec CanParseAs ts
recParsedToRecCanParseAs = rmap (parsedToCanParseAs . getCompose)
{-# INLINEABLE recParsedToRecCanParseAs #-}

-- | Helper for converting @Maybe (Parsed a)@ to  @CanParseAs a@
parsedToCanParseAs :: Maybe (Parsed a) -> CanParseAs a
parsedToCanParseAs Nothing = NoParse
parsedToCanParseAs (Just x)  = YesParse x
{-# INLINE parsedToCanParseAs #-}

-- | Type to hold parsing information for a single column
-- It starts completely unknown (@UnknownColType NoneMissing@)
-- and remains completely unknown if the data starts off missing
-- (@UnknownColType SomeMissing@).  Once we begin seeing non-missing
-- items we hold those results and whether or not we have encountered
-- any missing data, in @KnownColType@
data ColType ts = UnknownColType SomeMissing
                | KnownColType (SomeMissing, Rec CanParseAs ts)

type ColTH = Either (String -> Q [Dec]) Type
type ColTHF = Lift (->) CanParseAs (Const (Maybe ColTH))

-- | Given a 'Rec' of 'parseHow', build a @Rec@ of functions
-- which will map parse results for each type to the template-haskell
-- required to declare that type.
colTHs :: RMap ts
       => ParseHowRec ts
       -> Rec ColTHF ts
colTHs = rmap f where
  f :: ParseHow a -> ColTHF a
  f ParseHow{..} =  Lift $ \x -> Const $ case x of
    NoParse ->  Nothing
    YesParse a -> Just $ phRepresentableAsType a
{-# INLINE colTHs #-}

-- | Our default is always to declare a column as 'Text'
-- so we fall back to this in various situations where the
-- correct type is unclear.
fallbackText :: ColTH
fallbackText = Right (ConT (mkName "Text"))
{-# INLINE fallbackText #-}

instance ( RFoldMap ts
         , RMap ts
         , RApply ts)
     => ColumnTypeable (ColType ts) where
  type ParseType (ColType ts) = ParseResult ts
  type Parsers (ColType ts) = ParseHowRec ts
  colType = colTypeTH
  {-# INLINEABLE colType #-}
  inferType = parseResult'
  {-# INLINEABLE inferType #-}
  initialColType = UnknownColType NoneMissing
  {-# INLINE initialColType #-}
  updateWithParse = addParsedCell
  {-# INLINE updateWithParse #-}

-- | Given a 'Rec' of 'ParseHow' and the inference information in @ColType ts@,
-- produce the the template-haskell for the first type in the type-list @ts@ where
-- parsing succeeded.
-- This suggests an ordering of types in the list from most-to-least specific with
-- 'Text' always last.
-- NB: When using 'Categorical' this ordering requires some thought!  If you want
-- 'Categorical' for alternatives which cannot be parsed as anything but 'Text', you
-- should put your `Categorical'(s) (in increasing order of size) right before 'Text'
-- at the end of the list.  But if you have columns using integers to code Categorical
-- values you might want a Categorical *before* 'Int'.
colTypeTH :: ( RFoldMap ts
             , RMap ts
             , RApply ts)
          => ParseHowRec ts -> ColType ts -> Either (String -> Q [Dec]) Type
colTypeTH phR t =  case t of
    UnknownColType _ -> fallbackText
    KnownColType (_,ts) ->
      fromMaybe fallbackText $ getFirst $ rfoldMap (First . getConst) $ rapply (colTHs phR) ts
{-# INLINEABLE colTypeTH #-}

-- | Extract the 'SomeMissing' flag from a 'ColType'
colTypeSomeMissing :: ColType ts -> SomeMissing
colTypeSomeMissing (UnknownColType x) = x
colTypeSomeMissing (KnownColType (x, _)) = x
{-# INLINE colTypeSomeMissing #-}

-- | Given a @Rec ParseHow ts@, combine previous parsing information, a @ColType ts@ representing
-- possible parses of all the data seen so far, with the possible parses of a new piece of data,
-- represented by @ParseResult ts@ and produce a new set of possible parses for the entire column
-- so far.
addParsedCell :: (RMap ts, RApply ts) => ParseHowRec ts -> ColType ts -> ParseResult ts -> ColType ts
addParsedCell _ (UnknownColType _) MissingData = UnknownColType SomeMissing
addParsedCell _ (UnknownColType sm) (ParseResult pRec) = KnownColType (sm, pRec)
addParsedCell _ (KnownColType (_, ctRec)) MissingData = KnownColType (SomeMissing, ctRec)
addParsedCell phR (KnownColType (sm, ctRec)) (ParseResult pRec) = KnownColType (sm, newCtRec)
  where
    newCtRec = rzipWith3 combineCanParseAs phR pRec ctRec
{-# INLINEABLE addParsedCell #-}

-- | rzipWith extended to 3 argument functions
rzipWith3 :: forall f g h q ts. (RMap ts, RApply ts)
          => (forall x. f x -> g x -> h x -> q x)
          -> Rec f ts
          -> Rec g ts
          -> Rec h ts
          -> Rec q ts
rzipWith3 f fs gs = rapply appliedFG where
  appliedFG :: Rec (Lift (->) h q) ts
  appliedFG = rzipWith liftFG fs gs where
    liftFG :: f x -> g x -> Lift (->) h q x
    liftFG fx  = Lift . f fx
{-# INLINEABLE rzipWith3 #-}

data SimpleDict c a where
  SimpleDict :: c a => SimpleDict c a

-- | rzipWith but with a constraint in-scope when the zipping function is applied.
rzipWithC ::forall c ts f g h. (RMap ts, RApply ts, RPureConstrained c ts)
          => (forall x.c x => f x -> g x -> h x)
          -> Rec f ts
          -> Rec g ts
          -> Rec h ts
rzipWithC l = rzipWith3 q (rpureConstrained @c SimpleDict) where
  q :: SimpleDict c x -> f x -> g x -> h x
  q SimpleDict = l
{-# INLINEABLE rzipWithC #-}