packages feed

generic-data-functions-0.2.0: src/Generic/Data/Function/Traverse/Sum.hs

{-# LANGUAGE UndecidableInstances #-} -- required below GHC 9.6
{-# LANGUAGE AllowAmbiguousTypes #-} -- required due to generic typeclass design

module Generic.Data.Function.Traverse.Sum where

import GHC.Generics
import Generic.Data.Function.Util.Generic ( datatypeName', conName' )
import Generic.Data.Function.Traverse.Constructor ( GTraverseC(gTraverseC), GenericTraverse(..) )
import Generic.Data.Rep.Error
import Generic.Data.Function.Common

import Data.Text ( Text )
import Control.Applicative qualified as Applicative
import Control.Applicative ( Alternative((<|>)) )

{- | Sum type monads that can be generically 'traverse'd.

For sum types, we require a monad with choice to differentiate constructors.
-}
class (GenericTraverse f, Alternative f, Monad f) => GenericTraverseSum f where
    -- | Try to parse a prefix tag of type 'pt'.
    --
    -- Relevant metadata is provided as arguments.
    genericTraverseSumPfxTagAction
        :: GenericTraverseC f pt
        => String   -- ^ data type name
        -> f pt

    -- | Parse error due to no constructor matching the parsed prefix tag.
    --
    -- Relevant metadata is provided as arguments.
    genericTraverseSumNoMatchingCstrAction
        :: String   -- ^ data type name
        -> [String] -- ^ non-matching constructor names
        -> Text     -- ^ prefix tag, prettified
        -> f a

-- | How to use a type as a prefix tag in a generic sum type parser.
data PfxTagCfg a = PfxTagCfg
  { pfxTagCfgFromCstr :: String -> a
  -- ^ How to turn a constructor name into a prefix tag.

  , pfxTagCfgEq :: a -> a -> Bool
  -- ^ How to compare prefix tags for equality.
  --
  -- By shoving this into our generic derivation config, we can avoid adding an
  -- insidious 'Eq' constraint. In general, you will want to set this to '(==)'.

  , pfxTagCfgShow :: a -> Text
  -- ^ Make a prefix tag human-readable. 'show' is often appropriate.
  }

class GTraverseSum (opts :: SumOpts) cd f f' where
    gTraverseSum :: GenericTraverseC f pt => PfxTagCfg pt -> f (f' p)

instance (GenericTraverseSum f, GTraverseCSum cd f (l :+: r), Datatype cd)
  => GTraverseSum opts cd f (l :+: r) where
    gTraverseSum = gTraverseSum' @cd

gTraverseSum'
    :: forall {p} cd f f' pt
    .  (GenericTraverseC f pt, GenericTraverseSum f, GTraverseCSum cd f f', Datatype cd)
    => PfxTagCfg pt -> f (f' p)
gTraverseSum' ptc = do
    pt <- genericTraverseSumPfxTagAction cd
    gTraverseCSum @cd ptc pt <|> parseErrorNoMatch pt
  where
    cd = datatypeName' @cd
    parseErrorNoMatch pt =
        genericTraverseSumNoMatchingCstrAction cd testedCstrs ((pfxTagCfgShow ptc) pt)
    testedCstrs = [] -- TODO

instance GTraverseSum 'SumOnly cd f (C1 cc f') where
    gTraverseSum = error eNeedSum

instance (GenericTraverseSum f, GTraverseCSum cd f (C1 cc f'), Datatype cd)
  => GTraverseSum 'AllowSingletonSum cd f (C1 cc f') where
    gTraverseSum = gTraverseSum' @cd

instance GTraverseSum opts cd f V1 where
    gTraverseSum = error eNoEmpty

-- | Generic getter (constructor sum level).
class GTraverseCSum cd f f' where
    gTraverseCSum :: PfxTagCfg pt -> pt -> f (f' p)

instance (Functor f, Alternative f, GTraverseCSum cd f l, GTraverseCSum cd f r)
  => GTraverseCSum cd f (l :+: r) where
    gTraverseCSum ptc pt = l <|> r
      where
        l = L1 <$> gTraverseCSum @cd ptc pt
        r = R1 <$> gTraverseCSum @cd ptc pt

instance (Alternative f, GTraverseC cd cc 0 f f', Constructor cc)
  => GTraverseCSum cd f (C1 cc f') where
    gTraverseCSum ptc pt = do
        if   (pfxTagCfgEq ptc) pt ptCstr
        then M1 <$> gTraverseC @cd @cc @0
        else Applicative.empty
      where
        ptCstr = (pfxTagCfgFromCstr ptc) (conName' @cc)