wireform-arrow-0.1.0.0: src/Arrow/Derive.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{- | Annotation-driven Template Haskell deriver for Apache Arrow
'Arrow.Record.Table' values.
Arrow is a /columnar/ format: records become rows in a columnar
batch rather than recursive trees. The deriver therefore
produces row-level instances:
* For a record type, it emits a 'HasTable' instance so that
@'encodeTable' 'hasTable'@ and @'decodeTable' 'hasTable'@
round-trip a @'Data.Vector.Vector'@ of the record through an
Arrow 'Schema' + @'Data.Vector.Vector' 'ColumnArray'@.
* For a @newtype@, it emits 'HasEncoder' and 'HasDecoder'
instances that pass through to the inner type. This lets the
newtype be used as a record field by the record deriver.
Sum types and enums fail at splice time because Arrow has no
first-class column-level union representation.
Modifiers honoured (resolved against 'backendArrow'):
* 'rename', 'renameStyle', 'renameWith' — name of the column in
the emitted schema.
* 'skip' — drop the column entirely from both encode and decode
sides; if combined with 'defaults', the decoder fills the
field from the named default function.
* 'defaults' — the default value used for skipped fields when
decoding.
* 'coerced' — the field is encoded/decoded via the named target
type's 'HasEncoder' / 'HasDecoder'; @Data.Coerce.coerce@ wraps
the boundary in both directions.
-}
module Arrow.Derive (
-- * Class
HasTable (..),
-- * Derivers
deriveArrow,
deriveArrowTable,
deriveArrowColumn,
) where
import Arrow.Record (
Decoder,
Encoder,
Table,
columnD,
fieldE,
table,
)
import Arrow.Record.Generic (HasDecoder (..), HasEncoder (..))
import Data.Coerce (coerce)
import Data.Foldable (foldlM)
import Data.Functor.Contravariant (contramap)
import Data.Maybe (catMaybes)
import Data.Text qualified as T
import Language.Haskell.TH
import Wireform.Derive.Backend (backendArrow)
import Wireform.Derive.ModifierInfo
import Wireform.Derive.TypeInfo
-- ---------------------------------------------------------------------------
-- HasTable
-- ---------------------------------------------------------------------------
{- | A record type with a derived Arrow 'Table'.
Analogous to 'HasEncoder' / 'HasDecoder' from
"Arrow.Record.Generic", but operates at the row level rather
than the column level: a 'Table' bundles a 'RowEncoder' and a
'RowDecoder' for the same record type.
-}
class HasTable a where
hasTable :: Table a
-- ---------------------------------------------------------------------------
-- Public entry points
-- ---------------------------------------------------------------------------
{- | Derive Arrow instances for the named type.
Dispatches on the data shape:
* Records get a 'HasTable' instance.
* Newtypes get 'HasEncoder' + 'HasDecoder' instances that
pass through to the inner type.
* Sum types and enums fail at splice time.
-}
deriveArrow :: Name -> Q [Dec]
deriveArrow nm = do
ti <- reifyTypeInfo nm
case typeInfoShape ti of
TypeShapeRecord c -> deriveRecordTable ti c
TypeShapeNewtype c -> deriveNewtypeColumn ti c
TypeShapeEnum _ -> fail (rejectEnumOrSum nm "enum")
TypeShapeSum _ -> fail (rejectEnumOrSum nm "sum")
{- | Synonym for 'deriveArrow', used when calling sites want to
emphasise the record code path.
-}
deriveArrowTable :: Name -> Q [Dec]
deriveArrowTable = deriveArrow
{- | Synonym for 'deriveArrow', used when calling sites want to
emphasise the newtype (column) code path.
-}
deriveArrowColumn :: Name -> Q [Dec]
deriveArrowColumn = deriveArrow
rejectEnumOrSum :: Name -> String -> String
rejectEnumOrSum nm shape =
"Arrow.Derive: cannot derive Arrow instances for "
++ shape
++ " type "
++ show nm
++ ". Arrow has no first-class column-level union representation; "
++ "model alternatives as a discriminator + payload columns by hand."
-- ---------------------------------------------------------------------------
-- Records → HasTable
-- ---------------------------------------------------------------------------
deriveRecordTable :: TypeInfo -> ConInfo -> Q [Dec]
deriveRecordTable ti c = do
let typ = applyTypeArgs (ConT (typeInfoName ti)) (typeInfoVarTypes ti)
encExp <- buildRowEncoder c
decExp <- buildRowDecoder c
body <- [|table $(pure encExp) $(pure decExp)|]
let decl =
InstanceD
Nothing
[]
(AppT (ConT ''HasTable) typ)
[FunD 'hasTable [Clause [] (NormalB body) []]]
pure [decl]
{- | Build a 'RowEncoder' expression. Skipped fields drop out
entirely; if every field is skipped the encoder collapses to
'mempty' (a no-column batch).
-}
buildRowEncoder :: ConInfo -> Q Exp
buildRowEncoder c = do
pieces <- mapM mkFieldEncoder (conInfoFields c)
case catMaybes pieces of
[] -> [|mempty|]
(e : es) -> foldlM (\acc x -> [|$(pure acc) <> $(pure x)|]) e es
mkFieldEncoder :: FieldInfo -> Q (Maybe Exp)
mkFieldEncoder fi@(FieldInfo mSel _) = do
selName <- requireSelector mSel
mi <- reifyModifierInfoFor backendArrow selName
if miSkip mi
then pure Nothing
else do
let selBase = T.pack (nameBase selName)
keyExp <- renderWireKey mi selBase
enc <- case miCoerce mi of
Nothing ->
[|fieldE $(pure keyExp) $(varE selName) hasEncoder|]
Just tgtName ->
let tgtTy = ConT tgtName
srcTy = fieldInfoType fi
in [|
fieldE
$(pure keyExp)
( (coerce :: $(pure srcTy) -> $(pure tgtTy))
. $(varE selName)
)
(hasEncoder :: Encoder $(pure tgtTy))
|]
pure (Just enc)
{- | Build a 'RowDecoder' expression. Every field of the
constructor needs a slot in the applicative chain; skipped
fields use @pure <default>@ to fill in.
-}
buildRowDecoder :: ConInfo -> Q Exp
buildRowDecoder c = do
let conName = conInfoName c
fields = conInfoFields c
case fields of
[] -> [|pure $(conE conName)|]
(f0 : fs) -> do
e0 <- mkFieldDecoder f0
hd <- [|$(conE conName) <$> $(pure e0)|]
foldlM
( \acc f -> do
ef <- mkFieldDecoder f
[|$(pure acc) <*> $(pure ef)|]
)
hd
fs
mkFieldDecoder :: FieldInfo -> Q Exp
mkFieldDecoder fi@(FieldInfo mSel _) = do
selName <- requireSelector mSel
mi <- reifyModifierInfoFor backendArrow selName
if miSkip mi
then case miDefaults mi of
Just defNm ->
[|pure $(varE defNm)|]
Nothing ->
fail $
"Arrow.Derive: skipped field "
++ show selName
++ " requires a 'defaults' modifier so the decoder can "
++ "fill the missing value."
else do
let selBase = T.pack (nameBase selName)
keyExp <- renderWireKey mi selBase
case miCoerce mi of
Nothing ->
[|columnD $(pure keyExp) hasDecoder|]
Just tgtName ->
let tgtTy = ConT tgtName
srcTy = fieldInfoType fi
in [|
(coerce :: $(pure tgtTy) -> $(pure srcTy))
<$> columnD
$(pure keyExp)
(hasDecoder :: Decoder $(pure tgtTy))
|]
-- ---------------------------------------------------------------------------
-- Newtypes → HasEncoder / HasDecoder pass-through
-- ---------------------------------------------------------------------------
deriveNewtypeColumn :: TypeInfo -> ConInfo -> Q [Dec]
deriveNewtypeColumn ti c = do
let typ = applyTypeArgs (ConT (typeInfoName ti)) (typeInfoVarTypes ti)
innerTy <- case conInfoFields c of
[FieldInfo _ ty] -> pure ty
_ ->
fail $
"Arrow.Derive: newtype "
++ show (typeInfoName ti)
++ " must have exactly one field"
encBody <-
[|
contramap
(coerce :: $(pure typ) -> $(pure innerTy))
(hasEncoder :: Encoder $(pure innerTy))
|]
decBody <-
[|
(coerce :: $(pure innerTy) -> $(pure typ))
<$> (hasDecoder :: Decoder $(pure innerTy))
|]
let encInst =
InstanceD
Nothing
[]
(AppT (ConT ''HasEncoder) typ)
[FunD 'hasEncoder [Clause [] (NormalB encBody) []]]
decInst =
InstanceD
Nothing
[]
(AppT (ConT ''HasDecoder) typ)
[FunD 'hasDecoder [Clause [] (NormalB decBody) []]]
pure [encInst, decInst]
-- ---------------------------------------------------------------------------
-- Helpers
-- ---------------------------------------------------------------------------
requireSelector :: Maybe Name -> Q Name
requireSelector (Just n) = pure n
requireSelector Nothing =
fail
"Arrow.Derive: cannot derive Arrow instances for a positional \
\(non-record) constructor; Arrow columns are addressed by name."
applyTypeArgs :: Type -> [Type] -> Type
applyTypeArgs = foldl AppT