packages feed

strongweak-0.12.0: src/Strongweak/Strengthen/Generic.hs

{- | 'strengthen' over generic representations.

As with base instances, generic strengthening collates all failures rather than
short-circuiting on the first failure. Failures are annotated with precise
information describing where the failure occurred:

  * data type name
  * constructor name
  * field index
  * field name (if present)
-}

-- required due to type class design
{-# LANGUAGE AllowAmbiguousTypes, UndecidableInstances #-}

{-# LANGUAGE OverloadedStrings #-} -- required for failure

module Strongweak.Strengthen.Generic where

import Data.Kind ( type Constraint )
import Strongweak.Strengthen
import GHC.Generics
import Data.Kind ( Type )
import GHC.TypeNats ( Natural, type (+), KnownNat )
import Strongweak.Util.TypeNats ( natVal'' )
import Data.Text.Builder.Linear qualified as TBL
import GHC.Exts ( Symbol, fromString, proxy# )
import GHC.TypeLits ( KnownSymbol, symbolVal' )

-- | Strengthen a value generically.
--
-- The weak and strong types must be /compatible/. See 'Strongweak.Generic' for
-- the definition of compatibility in this context.
strengthenGeneric
    :: (Generic w, Generic s, GStrengthenD (Rep w) (Rep s))
    => w -> Either StrengthenFailure' s
strengthenGeneric = fmap to . gstrengthenD . from

-- | Generic strengthening at the datatype level.
class GStrengthenD w s where
    gstrengthenD :: w p -> Either StrengthenFailure' (s p)

-- | Strengthen a generic data type, replacing its metadata wrapping.
instance GStrengthenC wdn sdn w s
  => GStrengthenD
        (D1 (MetaData wdn _wmd2 _wmd3 _wmd4) w)
        (D1 (MetaData sdn _smd2 _smd3 _smd4) s) where
    gstrengthenD = fmap M1 . gstrengthenC @wdn @sdn . unM1

-- | Generic strengthening at the constructor sum level.
class GStrengthenC (wdn :: Symbol) (sdn :: Symbol) w s where
    gstrengthenC :: w p -> Either StrengthenFailure' (s p)

-- | Nothing to do for empty datatypes.
instance GStrengthenC wdn sdn V1 V1 where gstrengthenC = Right

-- | Strengthen sum types by casing and strengthening left or right.
instance
  ( GStrengthenC wdn sdn wl sl
  , GStrengthenC wdn sdn wr sr
  ) => GStrengthenC wdn sdn (wl :+: wr) (sl :+: sr) where
    gstrengthenC = \case L1 l -> L1 <$> gstrengthenC @wdn @sdn l
                         R1 r -> R1 <$> gstrengthenC @wdn @sdn r

-- | Enter a constructor, stripping its metadata wrapper.
instance (GStrengthenS 0 w s, ReifyCstrs wcd wcn scd scn)
  => GStrengthenC wcd scd
        (C1 (MetaCons wcn _wmc2 _wmc3) w)
        (C1 (MetaCons scn _smc2 _smc3) s) where
    gstrengthenC (M1 w) =
        case gstrengthenS @0 w of
          Right s -> Right (M1 s)
          Left  e -> failStrengthen [reifyCstrs @wcd @wcn @scd @scn] e

type ReifyCstrs :: Symbol -> Symbol -> Symbol -> Symbol -> Constraint
class ReifyCstrs ld lc rd rc where reifyCstrs :: TBL.Builder

-- | Special case: data type and constructor names are equivalent: simplify
instance {-# OVERLAPPING #-} (KnownSymbol d, KnownSymbol c)
  => ReifyCstrs d c d c where
    {-# INLINE reifyCstrs #-}
    reifyCstrs = d<>"."<>c
      where
        d = fromString (symbolVal' (proxy# @d))
        c = fromString (symbolVal' (proxy# @c))

instance (KnownSymbol ld, KnownSymbol lc, KnownSymbol rd, KnownSymbol rc)
  => ReifyCstrs ld lc rd rc where
    {-# INLINE reifyCstrs #-}
    reifyCstrs = ld<>"."<>lc<>" -> "<>rd<>"."<>rc
      where
        ld = fromString (symbolVal' (proxy# @ld))
        lc = fromString (symbolVal' (proxy# @lc))
        rd = fromString (symbolVal' (proxy# @rd))
        rc = fromString (symbolVal' (proxy# @rc))

-- | Generic strengthening at the constructor level.
class GStrengthenS (i :: Natural) w s where
    gstrengthenS :: w p -> Either [(TBL.Builder, StrengthenFailure')] (s p)

-- | Nothing to do for empty constructors.
instance GStrengthenS i U1 U1 where gstrengthenS = Right

-- | Strengthen product types by strengthening left and right.
instance
  ( GStrengthenS i                  wl sl
  , GStrengthenS (i + ProdArity wl) wr sr
  ) => GStrengthenS i (wl :*: wr) (sl :*: sr) where
    gstrengthenS (l :*: r) =
        -- With @Validation@, @A.liftA2 (:*:)@. But I don't have a use for it
        -- elsewhere, so let's just be explicit.
        case gstrengthenS @i l of
          Right la ->
            case gstrengthenS @(i + ProdArity wl) r of
              Right  ra -> Right (la :*: ra)
              Left   re -> Left re
          Left  le ->
            case gstrengthenS @(i + ProdArity wl) @_ @sr r of
              Right _ra -> Left le
              Left   re -> Left (le <> re)

-- | Special case: if source and target types are equivalent, just replace meta.
--
-- Note that we have to expand the metadata awkwardly for the overlapping
-- instances to work correctly. (There should be a better way to write this, but
-- it's purely style, so light TODO.)
instance {-# OVERLAPPING #-} GStrengthenS i
  (S1 (MetaSel _wms1 _wms2 _wms3 _wms4) (Rec0 a))
  (S1 (MetaSel _sms1 _sms2 _sms3 _sms4) (Rec0 a)) where
    gstrengthenS = Right . M1 . unM1

-- | Strengthen a field using the existing 'Strengthen' instance.
instance
  ( Weakened s ~ w -- required, else "illegal typesym family app in instance"
  , Strengthen s
  , ReifySelector i wmr smr
  ) => GStrengthenS i
        (S1 (MetaSel wmr _wms2 _wms3 _wms4) (Rec0 w))
        (S1 (MetaSel smr _sms2 _sms3 _sms4) (Rec0 s)) where
    gstrengthenS = unM1 .> unK1 .> strengthen .> \case
      Right s -> Right $ M1 $ K1 s
      Left  e -> Left  [(reifySelector @i @wmr @smr, e)]

{- TODO
* how to separate index and record name? @.@ is good and bad, uses same syntax
  as @dt.cstr@ for different reason BUT is pretty clear
* how to lay out precisely? fairly arbitrary
-}
class ReifySelector (i :: Natural) (l :: Maybe Symbol) (r :: Maybe Symbol) where
    reifySelector :: TBL.Builder

-- | Special case: both types had a record name, and they're equal
instance {-# OVERLAPPING #-} (KnownNat i, KnownSymbol lnm)
  => ReifySelector i (Just lnm) (Just lnm) where
    -- TODO check overlap works correct
    {-# INLINE reifySelector #-}
    reifySelector = i<>"."<>lnm
      where
        i   = fromString $ show $ natVal'' @i
        lnm = fromString $ symbolVal' (proxy# @lnm)

instance (KnownNat i, KnownSymbol lnm, KnownSymbol rnm)
  => ReifySelector i (Just lnm) (Just rnm) where
    {-# INLINE reifySelector #-}
    reifySelector = i<>"."<>lnm<>" -> "<>rnm
      where
        i   = fromString $ show $ natVal'' @i
        lnm = fromString $ symbolVal' (proxy# @lnm)
        rnm = fromString $ symbolVal' (proxy# @rnm)

instance KnownNat i => ReifySelector i Nothing Nothing where
    {-# INLINE reifySelector #-}
    reifySelector = fromString $ show $ natVal'' @i

instance (KnownNat i, KnownSymbol lnm)
  => ReifySelector i (Just lnm) Nothing where
    {-# INLINE reifySelector #-}
    reifySelector = i<>"."<>lnm
      where
        i   = fromString $ show $ natVal'' @i
        lnm = fromString $ symbolVal' (proxy# @lnm)

instance (KnownNat i, KnownSymbol rnm)
  => ReifySelector i Nothing (Just rnm) where
    {-# INLINE reifySelector #-}
    reifySelector = i<>" -> "<>rnm
      where
        i   = fromString $ show $ natVal'' @i
        rnm = fromString $ symbolVal' (proxy# @rnm)

--------------------------------------------------------------------------------

-- from flow
(.>) :: (a -> b) -> (b -> c) -> a -> c
f .> g = g . f

--------------------------------------------------------------------------------

-- could define this with @Generic.Type.Function.FoldMap.GTFoldMapC (+) 0 _@...
-- but pretty dumb LOL
type family ProdArity (f :: k -> Type) :: Natural where
    ProdArity (S1 c f)  = 1
    ProdArity (l :*: r) = ProdArity l + ProdArity r