packages feed

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

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeOperators #-}
-- | Code generation of types relevant to Frames use-cases. Generation
-- may be driven by an automated inference process or manual use of
-- the individual helpers.
module Frames.Streamly.TH
  (
    -- * Declare Individual Column Types
    declareColumn
  , declareColumnType
  , declarePrefixedColumn
  , declarePrefixedColumnType
    -- * Control Type-declarations for a table
  , RowGen(..)
  , Separator(..)
  , defaultSep
  , defaultIsMissing
  , DefaultStream
  , OrMissingWhen(..)
  , setOrMissingWhen
  , rowGen
  , rowGenCat
  , modifyColumnSelector
  , modifyRowTypeNameAndColumnSelector
  , allColumnsAsNamed
  , noHeaderColumnsNumbered
  , prefixColumns
  , prefixAsNamed
  , columnSubset
  , renamedHeaderSubset
  , renameSomeUsingNames
  , renameSomeUsingPositions
  , namedColumnNumberSubset
  , namesGiven
    -- * Declare all necessary types for a table
  , tableTypes
  , tableTypes'
  , tableTypesText'
  , ColumnId(..)
    -- * Header text vs. Type name text
  , ColTypeName(..)
  , HeaderText(..)
    -- * Re-exports
  , module Frames.Streamly.OrMissing
  )
where

import Prelude hiding (lift)
#if MIN_VERSION_streamly(0,9,0)
import Frames.Streamly.Streaming.Streamly (StreamlyStream, Stream)
#else
import Frames.Streamly.Streaming.Streamly (StreamlyStream, SerialT)
#endif
import Frames.Streamly.Streaming.Pipes (PipeStream)
import Frames.Streamly.Streaming.Class
import Frames.Streamly.Streaming.Common (Separator(..))
import qualified Frames.Streamly.CSV as SCSV
import Frames.Streamly.CSV (ParserOptions(..), defaultSep) -- for re-export or TH
import Frames.Streamly.OrMissing
import qualified Frames.Streamly.ColumnUniverse as FSCU
import qualified Frames.Streamly.ColumnTypeable as FSCT

import qualified Frames.Streamly.Internal.CSV as ICSV
import Frames.Streamly.Internal.CSV (ColumnId(..), HeaderText(..) ,ColTypeName(..), OrMissingWhen(..))

--import qualified Frames.CSV as Frames (defaultParser, ParserOptions(columnSeparator))

import Data.Char (toLower)
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.Vinyl
import Data.Vinyl.TypeLevel (RIndex)
import Frames.Col ((:->))
import Frames.Rec(Record)
import Frames.Utils
import Language.Haskell.TH hiding (Type)
import qualified Language.Haskell.TH as TH (Type)
import Language.Haskell.TH.Syntax hiding (Type)


--type DefaultStream = StreamlyStream SerialT
#if MIN_VERSION_streamly(0,9,0)
type DefaultStream = StreamlyStream Stream
#else
type DefaultStream = StreamlyStream SerialT
#endif

-- | Generate a column type.
recDec :: [TH.Type] -> TH.Type
recDec = AppT (ConT ''Record) . go
  where go [] = PromotedNilT
        go (t:cs) = AppT (AppT PromotedConsT t) (go cs)

-- | Declare a type synonym for a column.
mkColSynDec :: TypeQ -> Name -> DecQ
mkColSynDec colTypeQ colTName = tySynD colTName [] colTypeQ

-- | Declare lenses for working with a column.
mkColLensDec :: Name -> TH.Type -> T.Text -> DecsQ
mkColLensDec colTName colTy colPName = sequenceA [tySig, val, tySig', val']
  where nm = mkName $ T.unpack colPName
        nm' = mkName $ T.unpack colPName <> "'"
        -- tySig = sigD nm [t|Proxy $(conT colTName)|]
        tySig = sigD nm [t|forall f rs.
                           (Functor f,
                            RElem $(conT colTName) rs (RIndex $(conT colTName) rs))
                         => ($(pure colTy) -> f $(pure colTy))
                         -> Record rs
                         -> f (Record rs)
                         |]
        tySig' = sigD nm' [t|forall f g rs.
                            (Functor f,
                             RElem $(conT colTName) rs (RIndex $(conT colTName) rs))
                          => (g $(conT colTName) -> f (g $(conT colTName)))
                          -> Rec g rs
                          -> f (Rec g rs)
                          |]
        val = valD (varP nm)
                   (normalB [e|rlens @($(conT colTName)) . rfield |])
                   []
        val' = valD (varP nm')
                    (normalB [e|rlens' @($(conT colTName))|])
                    []

lowerHead :: T.Text -> Maybe T.Text
lowerHead = fmap aux . T.uncons
  where aux (c,t) = T.cons (toLower c) t

-- | For each column, we declare a type synonym for its type, and a
-- Proxy value of that type.
colDec :: T.Text -> String -> T.Text -> Bool
       -> Either (String -> Q [Dec]) TH.Type
       -> Q (TH.Type, [Dec])
colDec prefix rowName colName addOrMissing colTypeGen = do
  (colTy', extraDecs) <- either colDecsHelper (pure . (,[])) colTypeGen
  let colTy = addOrMissingToTypeIf addOrMissing colTy'
      colTypeQ = [t|$(litT . strTyLit $ T.unpack colName) :-> $(return colTy)|]
  syn <- mkColSynDec colTypeQ colTName'
  lenses <- mkColLensDec colTName' colTy colPName
  return (ConT colTName', syn : extraDecs ++ lenses)
  where addOrMissingToTypeIf :: Bool -> TH.Type -> TH.Type
        addOrMissingToTypeIf b t = if b then AppT (ConT ''OrMissing) t else t
        colTName = sanitizeTypeName (prefix <> capitalize1 colName)
        colPName = fromMaybe "colDec impossible" (lowerHead colTName)
        colTName' = mkName $ T.unpack colTName
        colDecsHelper f =
          let qualName = rowName ++ T.unpack (capitalize1 colName)
          in (ConT (mkName qualName),) <$> f qualName

-- | Splice for manually declaring a column of a given type. For
-- example, @declareColumn "x2" ''Double@ will declare a type synonym
-- @type X2 = "x2" :-> Double@ and a lens @x2@.
declareColumn :: T.Text -> Name -> DecsQ
declareColumn = flip declarePrefixedColumn T.empty

-- | Splice for manually declaring a column of a given type. Works
-- like 'declareColumn', but uses a quote for the payload type
-- rather than a specific 'Name'. This lets you declare a column with
-- a type like, @Maybe Int@, where @Maybe Int@ is a 'Type' but not a
-- 'Name'. For example, with @-XOverloadedStrings@ and
-- @-XQuasiQuotes@,
--
-- > declareColumnType "x2" [t|Maybe Int|]
declareColumnType :: T.Text -> Q TH.Type -> DecsQ
declareColumnType n = declarePrefixedColumnType n T.empty

-- | Splice for manually declaring a column of a given type in which
-- the generated type synonym's name has a prefix applied to the
-- column name. For example, @declarePrefixedColumn "x2" "my"
-- ''Double@ will declare a type synonym @type MyX2 = "x2" :-> Double@
-- and a lens @myX2@.
declarePrefixedColumn :: T.Text -> T.Text -> Name -> DecsQ
declarePrefixedColumn colName prefix colTypeName =
  (:) <$> mkColSynDec colTypeQ colTName'
      <*> mkColLensDec colTName' colTy colPName
  where prefix' = capitalize1 prefix
        colTName = sanitizeTypeName (prefix' <> capitalize1 colName)
        colPName = fromMaybe "colDec impossible" (lowerHead colTName)
        colTName' = mkName $ T.unpack colTName
        colTy = ConT colTypeName
        colTypeQ = [t|$(litT . strTyLit $ T.unpack colName) :-> $(return colTy)|]

-- | Splice for manually declaring a column of a given type. Works
-- like 'declarePrefixedColumn', but uses a quote for the payload type
-- rather than a specific 'Name'. This lets you declare a column with
-- a type like, @Maybe Int@, where @Maybe Int@ is a 'Type' but not a
-- 'Name'. For example, with @-XOverloadedStrings@ and
-- @-XQuasiQuotes@,
--
-- > declarePrefixedColumnType "x2" "my" [t|Maybe Int|]
declarePrefixedColumnType :: T.Text -> T.Text -> Q TH.Type -> DecsQ
declarePrefixedColumnType colName prefix payloadType =
  (:) <$> mkColSynDec colTypeQ colTName'
      <*> (payloadType >>= \colTy -> mkColLensDec colTName' colTy colPName)
  where prefix' = capitalize1 prefix
        colTName = sanitizeTypeName (prefix' <> capitalize1 colName)
        colPName = fromMaybe "colDec impossible" (lowerHead colTName)
        colTName' = mkName $ T.unpack colTName
        colTypeQ = [t|$(litT . strTyLit $ T.unpack colName) :-> $payloadType|]

-- * Default CSV Parsing

-- | Control how row and named column types are generated.
-- The first type argument indicates the type of stream used for underlying file loading
-- The second type argument is @Text@ or @Int@ depending how columns are indexed.
-- The third type argument is a type-level list of the possible column types.
data RowGen (s :: (Type -> Type) -> Type -> Type) (b :: ColumnId) (a :: [Type]) =
  RowGen { genColumnSelector    :: ICSV.RowGenColumnSelector b
           -- ^ Use these column names. If empty, expect a
           -- header row in the data file to provide
           -- column names.
         , tablePrefix    :: String
           -- ^ A common prefix to use for every generated
           -- declaration.
         , separator      :: SCSV.Separator
           -- ^ The string that separates the columns on a
           -- row.
         , quotingMode :: SCSV.QuotingMode
           -- ^ Special handling for quoted fields
         , rowTypeName    :: String
           -- ^ The row type that enumerates all
           -- columns.
         , columnParsers :: FSCU.ParseHowRec a
           -- ^ A record field that mentions the phantom type list of
           -- possible column types. Having this field prevents record
           -- update syntax from losing track of the type argument.
         , inferencePrefix :: Int
           -- ^ Number of rows to inspect to infer a type for each
           -- column. Defaults to 1000.
         , isMissing :: Text -> Bool
           -- ^ Control what text is considered missing.
           -- Defaults to @isMissing t = null t || t == "NA"@
         , lineReader :: SCSV.Separator -> s (IOSafe s IO) [Text]
           -- ^ A producer of rows of ’T.Text’ values that were
           -- separated by a 'Separator' value.
         }

defaultIsMissing :: Text -> Bool
defaultIsMissing t = T.null t || t == "NA"

-- | A default 'RowGen'. This instructs the type inference engine to
-- get column names from the data file, use the default column
-- separator (a comma), infer column types from the default 'Columns'
-- set of types, and produce a row type with name @Row@.
-- Polymorphic in the stream type
rowGen' :: forall s. StreamFunctionsIO s IO => FilePath -> RowGen s 'ColumnByName FSCU.CommonColumns
rowGen' fp = RowGen
  allColumnsAsNamed \
  ""
  SCSV.defaultSep
  SCSV.defaultQuotingMode
  "Row"
  FSCU.parseableParseHowRec
  1000
  defaultIsMissing
  (\sep -> sTokenized @s @IO sep SCSV.defaultQuotingMode fp)
{-# INLINEABLE rowGen' #-}

-- | A default 'RowGen'. This instructs the type inference engine to
-- get column names from the data file, use the default column
-- separator (a comma), infer column types from the default 'Columns'
-- set of types, and produce a row type with name @Row@.
-- Default stream type.
rowGen :: FilePath -> RowGen DefaultStream 'ColumnByName FSCU.CommonColumns
rowGen = rowGen' @DefaultStream
{-RowGen
  allColumnsAsNamed \
  ""
  SCSV.defaultSep
  "Row"
  FSCU.parseableParseHowRec
  1000
  defaultIsMissing
  (SCSV.streamTokenized' @DefaultStream fp SCSV.defaultSep)
-}
{-# INLINEABLE rowGen #-}


-- | Like 'rowGen\'', but will also generate custom data types for
-- 'Categorical' variables with up to 8 distinct variants.
rowGenCat' :: forall s. StreamFunctionsIO s IO => FilePath -> RowGen s 'ColumnByName FSCU.CommonColumnsCat
rowGenCat' fp = RowGen
  allColumnsAsNamed
  ""
  SCSV.defaultSep
  SCSV.defaultQuotingMode
  "Row"
  FSCU.parseableParseHowRec
  1000
  defaultIsMissing
  (\sep -> sTokenized @s @IO sep SCSV.defaultQuotingMode fp)
{-# INLINEABLE rowGenCat' #-}

-- | Like 'rowGen', but will also generate custom data types for
-- 'Categorical' variables with up to 8 distinct variants.
rowGenCat :: FilePath -> RowGen DefaultStream 'ColumnByName FSCU.CommonColumnsCat
rowGenCat = rowGenCat' @DefaultStream
{-# INLINEABLE rowGenCat #-}

-- | Update or replace the columnHandler in a RowGen
modifyColumnSelector :: forall a b s x.(ICSV.RowGenColumnSelector a -> ICSV.RowGenColumnSelector b) -> RowGen s a x -> RowGen s b x
modifyColumnSelector f rg =
  let newColHandler :: ICSV.RowGenColumnSelector b = f (genColumnSelector rg)
  in rg { genColumnSelector = newColHandler }
{-# INLINEABLE modifyColumnSelector #-}

-- | Update or replace the columnHandler in a RowGen
modifyRowTypeNameAndColumnSelector :: Text -> (ICSV.RowGenColumnSelector a -> ICSV.RowGenColumnSelector b) -> RowGen s a x -> RowGen s b x
modifyRowTypeNameAndColumnSelector newRowName f rg =
  let newColHandler = f (genColumnSelector rg)
  in rg { rowTypeName = T.unpack newRowName, genColumnSelector = newColHandler }
{-# INLINEABLE modifyRowTypeNameAndColumnSelector #-}


-- | Default Column Handler. Declare one type per column.
-- Use the header to generate names.
allColumnsAsNamed :: ICSV.RowGenColumnSelector 'ColumnByName
allColumnsAsNamed = ICSV.GenUsingHeader (\x -> ICSV.Include (ICSV.ColTypeName $ ICSV.headerText x, ICSV.NeverMissing)) (const [])
{-# INLINEABLE allColumnsAsNamed #-}

-- | Helper for declaring column types from a file with no header.
noHeaderColumnsNumbered' :: ICSV.RowGenColumnSelector 'ColumnByPosition
noHeaderColumnsNumbered' = ICSV.GenWithoutHeader (\n -> ICSV.Include (ICSV.ColTypeName $ show n, ICSV.NeverMissing)) (const [])
{-# INLINEABLE noHeaderColumnsNumbered' #-}

-- | Use a given prefix and append the column number to generate column types for a file with no header.
noHeaderColumnsNumbered :: Text -> ICSV.RowGenColumnSelector 'ColumnByPosition
noHeaderColumnsNumbered prefix = prefixColumns prefix noHeaderColumnsNumbered'
{-# INLINEABLE noHeaderColumnsNumbered #-}

-- | Add a prefix to all the generated column type names.
prefixColumns :: Text -> ICSV.RowGenColumnSelector a -> ICSV.RowGenColumnSelector a
prefixColumns p ch = ICSV.modifyColumnSelector ch g id where
  g f = \x -> case f x of
    ICSV.Exclude -> ICSV.Exclude
    ICSV.Include (t, mw) -> ICSV.Include (ICSV.ColTypeName (p <> ICSV.colTypeName t), mw)
{-# INLINEABLE prefixColumns #-}

-- | Generate all column type names from the header but add a prefix.
prefixAsNamed :: Text -> ICSV.RowGenColumnSelector 'ColumnByName
prefixAsNamed p = prefixColumns p allColumnsAsNamed
{-# INLINEABLE prefixAsNamed #-}

inSetButNotList :: Ord a => Set a -> [a] -> [a]
inSetButNotList s = Set.toList . Set.difference s . Set.fromList
{-# INLINE inSetButNotList #-}

inMapKeysButNotList :: Ord a => Map a v -> [a] -> [a]
inMapKeysButNotList m = inSetButNotList (Map.keysSet m)
{-# INLINE inMapKeysButNotList #-}

-- | Generate types for only a subset of the columns.
-- Generated 'ParserOptions' will select the correct columns when loading.
columnSubset :: Ord (ICSV.ColumnIdType a) => Set (ICSV.ColumnIdType a) -> ICSV.RowGenColumnSelector a -> ICSV.RowGenColumnSelector a
columnSubset s rgch = ICSV.modifyColumnSelector rgch g h where
  g f = \x -> if x `Set.member` s then f x else ICSV.Exclude
  h mrF cids = mrF cids ++ inSetButNotList s cids
{-# INLINEABLE columnSubset #-}

-- | Generate Column Type Names for only the headers given in the map.  Use
-- the names given as values in the map rather than those in the header.
-- Generated 'ParserOptions' will select the correct columns when loading.
renamedHeaderSubset :: Map ICSV.HeaderText ICSV.ColTypeName -> ICSV.RowGenColumnSelector 'ColumnByName
renamedHeaderSubset renamedS = ICSV.GenUsingHeader f mrF where
  f x = maybe ICSV.Exclude g $ Map.lookup x renamedS
  mrF = inMapKeysButNotList renamedS
  g x = ICSV.Include (x, ICSV.NeverMissing)
{-# INLINEABLE renamedHeaderSubset #-}


-- | rename some column types while leaving the rest alone.
renameSome ::  Ord (ICSV.ColumnIdType a)
           => Map (ICSV.ColumnIdType a) ICSV.ColTypeName --, ICSV.OrMissingWhen)
           -> ICSV.RowGenColumnSelector a
           -> ICSV.RowGenColumnSelector a
renameSome m rgcs = ICSV.modifyColumnSelector rgcs g h where
  wom = \case
    ICSV.Exclude -> NeverMissing
    ICSV.Include (_, x) -> x
  g f cid = case Map.lookup cid m of
    Nothing -> f cid
    Just x -> ICSV.Include (x, wom $ f cid)
  h mrF cids = mrF cids ++ inMapKeysButNotList m cids
{-# INLINEABLE renameSome #-}

-- | rename some column types while leaving the rest alone.
renameSomeUsingNames :: Map ICSV.HeaderText ICSV.ColTypeName --, ICSV.OrMissingWhen)
                     -> ICSV.RowGenColumnSelector 'ICSV.ColumnByName
                     -> ICSV.RowGenColumnSelector 'ICSV.ColumnByName
renameSomeUsingNames = renameSome
{-# INLINEABLE renameSomeUsingNames #-}

-- | rename some column types while leaving the rest alone.
renameSomeUsingPositions :: Map Int ICSV.ColTypeName
                         -> ICSV.RowGenColumnSelector 'ICSV.ColumnByPosition
                         -> ICSV.RowGenColumnSelector 'ICSV.ColumnByPosition
renameSomeUsingPositions = renameSome
{-# INLINEABLE renameSomeUsingPositions #-}

-- | Generate Column Type Names for only the given numbered Columns using
-- names given as values in the map.  Can ignore a header row or work without one
-- as set by the first parameter.
-- Generated 'ParserOptions' will select the correct columns when loading.
namedColumnNumberSubset :: Bool -> Map Int ICSV.ColTypeName -> ICSV.RowGenColumnSelector 'ColumnByPosition
namedColumnNumberSubset hasHeader namedS =
  if hasHeader
    then ICSV.GenIgnoringHeader f mrF
    else ICSV.GenWithoutHeader f mrF
  where
    f n = maybe ICSV.Exclude g $ Map.lookup n namedS
    g x = ICSV.Include (x, ICSV.NeverMissing)
    mrF = inMapKeysButNotList namedS
{-# INLINEABLE namedColumnNumberSubset #-}

-- | Use the given names to declare Column Type Names for the
-- first N (= length of the given list of names) columns of the given file.
-- Generated 'ParserOptions' will select the correct columns when loading.
namesGiven :: Bool -> [ICSV.ColTypeName] -> ICSV.RowGenColumnSelector 'ColumnByPosition
namesGiven hasHeader names = namedColumnNumberSubset hasHeader m
  where
    m = Map.fromList $ zip [0..] names
{-# INLINEABLE namesGiven #-}

setOrMissingWhen :: (Eq (ICSV.ColumnIdType b)) => ICSV.ColumnIdType b -> ICSV.OrMissingWhen -> RowGen s b a -> RowGen s b a
setOrMissingWhen cid mw rg = rg { genColumnSelector = newColSelector } where
  colSelector = genColumnSelector rg
  f oldF x = if x /= cid
             then oldF x
             else case oldF x of
                    ICSV.Exclude -> ICSV.Exclude
                    ICSV.Include (ctn, _) -> ICSV.Include (ctn, mw)
  newColSelector = ICSV.modifyColumnSelector colSelector f id
{-# INLINEABLE setOrMissingWhen #-}

-- -- | Generate a type for each row of a table. This will be something
-- -- like @Record ["x" :-> a, "y" :-> b, "z" :-> c]@.
-- tableType :: String -> FilePath -> DecsQ
-- tableType n fp = tableType' (rowGen fp) { rowTypeName = n }

-- | Like 'tableType', but additionally generates a type synonym for
-- each column, and a proxy value of that type. If the CSV file has
-- column names \"foo\", \"bar\", and \"baz\", then this will declare
-- @type Foo = "foo" :-> Int@, for example, @foo = rlens \@Foo@, and
-- @foo' = rlens' \@Foo@.
tableTypes :: String -> FilePath -> DecsQ
tableTypes n fp = tableTypes' (rowGen fp) { rowTypeName = n }

-- * Customized Data Set Parsing

-- | Generate a type for a row of a table. This will be something like
-- @Record ["x" :-> a, "y" :-> b, "z" :-> c]@.  Column type synonyms
-- are /not/ generated (see 'tableTypes'').
-- tableType' :: forall a. (ColumnTypeable a, Monoid a)
--            => RowGen a -> DecsQ
-- tableType' (RowGen {..}) =
--     pure . TySynD (mkName rowTypeName) [] <$>
--     (runIO (P.runSafeT (readColHeaders opts lineSource)) >>= recDec')
--   where recDec' = recDec . map (second colType) :: [(T.Text, a)] -> Q Type
--         colNames' | null columnNames = Nothing
--                   | otherwise = Just (map T.pack columnNames)
--         opts = ParserOptions colNames' separator (RFC4180Quoting '\"')
--         lineSource = lineReader separator >-> P.take prefixSize

-- | Tokenize the first line of a ’Streamly.SerialT’.
colNamesP :: (Monad m, StreamFunctions s m) => s m [Text] -> m [T.Text]
colNamesP src = fromMaybe [] <$> sHead src

-- | Generate a type for a row of a table all of whose columns remain
-- unparsed 'Text' values.
tableTypesText' :: forall s b a.StreamFunctionsIO s IO
                => RowGen s b a
                -> DecsQ
tableTypesText' RowGen {..} = do
  firstRow <- runIO $ runSafe @s $ colNamesP $ lineReader separator
  let (allColStates, pch) = case genColumnSelector of
        ICSV.GenUsingHeader f _ ->
          let allHeaders = ICSV.HeaderText <$> firstRow
              allColStates' = f <$> allHeaders
          in (allColStates', ICSV.colStatesAndHeadersToParseColHandler allColStates' allHeaders)
        ICSV.GenIgnoringHeader f _ ->
          let allHeaders = ICSV.HeaderText <$> firstRow
              allIndexes = [0..(length allHeaders - 1)]
              allColStates' = f <$> allIndexes
          in (allColStates', ICSV.ParseIgnoringHeader allColStates')
        ICSV.GenWithoutHeader f _ ->
          let allIndexes = [0..(length firstRow - 1)]
              allColStates' = f <$> allIndexes
          in (allColStates', ICSV.ParseWithoutHeader allColStates')
      colNames = ICSV.includedColTypeNames allColStates

  let opts = SCSV.ParserOptions pch separator (SCSV.RFC4180Quoting '\"')
  let colNamesT = zip (fmap ICSV.colTypeName colNames) (repeat (ConT ''T.Text))
  (colTypes, colDecs) <- second concat . unzip
                         <$> mapM (uncurry mkColDecs) colNamesT
  let recTy = TySynD (mkName rowTypeName) [] (recDec colTypes)
      optsName = case rowTypeName of
                   [] -> error "Row type name shouldn't be empty"
                   h:t -> mkName $ toLower h : t ++ "Parser"
  optsTy <- sigD optsName [t|ParserOptions|]
  optsDec <- valD (varP optsName) (normalB $ lift opts) []
  return (recTy : optsTy : optsDec : colDecs)
  where mkColDecs colNm colTy = do
          let safeName = T.unpack (sanitizeTypeName colNm)
          mColNm <- lookupTypeName (tablePrefix ++ safeName)
          case mColNm of
            Just n -> pure (ConT n, [])
            Nothing -> colDec (T.pack tablePrefix) rowTypeName colNm False (Right colTy)

-- | Generate a type for a row of a table. This will be something like
-- @Record ["x" :-> a, "y" :-> b, "z" :-> c]@. Additionally generates
-- a type synonym for each column, and a proxy value of that type. If
-- the CSV file has column names \"foo\", \"bar\", and \"baz\", then
-- this will declare @type Foo = "foo" :-> Int@, for example, @foo =
-- rlens \@Foo@, and @foo' = rlens' \@Foo@.
tableTypes' :: forall ts b s.
               (FSCT.ColumnTypeable (FSCU.ColType ts)
               , Show (ICSV.ColumnIdType b)
               , StreamFunctionsIO s IO)
            => RowGen s b ts -> DecsQ
tableTypes' RowGen {..} = do
  (typedCols, pch) <- runIO
                      $ runSafe @s
                      $ SCSV.readColHeaders columnParsers genColumnSelector lineSource :: Q ([SCSV.ColTypeInfo (FSCU.ColType ts)], ICSV.ParseColumnSelector)
--  let colDecsFromTypedCols :: (ICSV.ColTypeName, FSCU.ColType ts)
--      colDecsFromTypedCols
  (colTypes, colDecs) <- (second concat . unzip)
                         <$> mapM mkColDecs typedCols
  let recTy = TySynD (mkName rowTypeName) [] (recDec colTypes)
      opts = SCSV.ParserOptions pch separator (SCSV.RFC4180Quoting '\"')
      optsName = case rowTypeName of
                   [] -> error "Row type name shouldn't be empty"
                   h:t -> mkName $ toLower h : t ++ "Parser"
  optsTy <- sigD optsName [t|ParserOptions|]
  optsDec <- valD (varP optsName) (normalB $ lift opts) []
  return (recTy : optsTy : optsDec : colDecs)
  where lineSource :: s (IOSafe s IO) [Text]
        lineSource = sTake inferencePrefix $ lineReader separator
        inferMaybe :: ICSV.OrMissingWhen -> FSCU.SomeMissing -> Bool
        inferMaybe mw sm = case mw of
          ICSV.NeverMissing -> False
          ICSV.AlwaysPossible -> True
          ICSV.IfSomeMissing -> sm == FSCU.SomeMissing
        mkColDecs :: SCSV.ColTypeInfo (FSCU.ColType ts) -> Q (TH.Type, [Dec])
        mkColDecs (SCSV.ColTypeInfo colNm colMW colTy) = do
          let safeName = tablePrefix ++ (T.unpack . sanitizeTypeName . ICSV.colTypeName $ colNm) -- ??
              colTyTH = FSCT.colType columnParsers colTy
              isMaybe = inferMaybe colMW (FSCU.colTypeSomeMissing colTy)
          mColNm <- lookupTypeName safeName
          case mColNm of
            Just n -> pure (ConT n, []) -- Column's type was already defined
            Nothing -> colDec (T.pack tablePrefix) rowTypeName (ICSV.colTypeName colNm) isMaybe colTyTH