packages feed

imsos-monad-0.2.4.0: src/Data/Comp/ProjectionExt.hs

{-# LANGUAGE ConstraintKinds        #-}
{-# LANGUAGE DataKinds              #-}
{-# LANGUAGE FlexibleContexts       #-}
{-# LANGUAGE FlexibleInstances      #-}
{-# LANGUAGE KindSignatures         #-}
{-# LANGUAGE MultiParamTypeClasses  #-}
{-# LANGUAGE ScopedTypeVariables    #-}
{-# LANGUAGE TypeFamilies           #-}
{-# LANGUAGE TypeOperators          #-}
{-# LANGUAGE UndecidableInstances   #-}
{-# LANGUAGE TypeApplications       #-}

-- ---------------------------------------------------------------------------
-- Module      :  Data.Comp.ProjectionExt
--
-- Extends Data.Comp.Projection with:
--
--   1. Support for (:*:) products alongside tuples (,).
--      Because Elem is a closed type family in the original module,
--      we re-implement it here with the extra cases, and re-derive
--      everything on top of it.  The original (:<) / pr are NOT
--      re-exported; use (:<~) and pr~ from this module instead.
--
--   2. uncons — projects a component AND returns the remainder
--      of the product with that component removed, as an abstract type.
--
--   3. recons — rebuilds the original product from a remainder
--      and a (possibly modified) projected component.
--
-- Supported product constructors:
--   (,)   — Haskell built-in pair
--   (:*:) — GHC.Generics / Data.Functor.Product style functor product
--           (used at kind Type by applying both sides to a common index)
--
-- Adding further product constructors (e.g. Data.Strict.Pair) requires
-- one new equation in Elem, one in Remainder2, and two new instances
-- each in Proj, ProjR, and Reconstruct.
-- ---------------------------------------------------------------------------

module Data.Comp.ProjectionExt
  ( -- * Constraint and plain projection (replaces :< / pr)
    (:<~), (:<|), (:*:)(..)
  , pr
    -- * Projection with remainder
  , RemainderOf
  , uncons
  , recons
  , modify
    -- * Re-exported position machinery (for advanced use)
  , Elem
  ) where

import Data.Comp.SubsumeCommon
import Data.Kind    (Type)

data a :*: b = a :*: b
infixr 6 :*:

-- ---------------------------------------------------------------------------
-- 1.  Elem — closed type family recognising BOTH (,) and (:*:)
--
-- The structure exactly mirrors the original Elem, with every
-- tuple case duplicated for (:*:).
-- ---------------------------------------------------------------------------

type family Elem (f :: Type) (g :: Type) :: Emb where

  -- Base: exact match
  Elem f f = 'Found 'Here

  -- LHS is a tuple-pair: look for each component independently in g
  Elem (f1, f2) g = Sum' (Elem f1 g) (Elem f2 g)

  -- LHS is a (:*:)-pair: same idea
  Elem (f1 :*: f2) g = Sum' (Elem f1 g) (Elem f2 g)

  -- RHS is a tuple-pair: choose which side f lives in
  Elem f (g1, g2) = Choose (Elem f g1) (Elem f g2)

  -- RHS is a (:*:)-pair: same idea
  Elem f (g1 :*: g2) = Choose (Elem f g1) (Elem f g2)

  -- No match
  Elem f g = 'NotFound

-- ---------------------------------------------------------------------------
-- 2.  Proj — plain projection, mirrors the original Proj class
--     but dispatches on both (,) and (:*:) at the value level.
-- ---------------------------------------------------------------------------

class Proj (e :: Emb) (p :: Type) (q :: Type) where
  proj' :: Proxy e -> q -> p

-- Base: p is q
instance Proj ('Found 'Here) f f where
  proj' _ x = x

-- p is in the left branch of a tuple
instance Proj ('Found pos) f g
    => Proj ('Found ('Le pos)) f (g, g') where
  proj' _ (l, _) = proj' (P :: Proxy ('Found pos)) l

-- p is in the right branch of a tuple
instance Proj ('Found pos) f g
    => Proj ('Found ('Ri pos)) f (g', g) where
  proj' _ (_, r) = proj' (P :: Proxy ('Found pos)) r

-- p is in the left branch of a (:*:)
instance Proj ('Found pos) f g
    => Proj ('Found ('Le pos)) f (g :*: g') where
  proj' _ (l :*: _) = proj' (P :: Proxy ('Found pos)) l

-- p is in the right branch of a (:*:)
instance Proj ('Found pos) f g
    => Proj ('Found ('Ri pos)) f (g' :*: g) where
  proj' _ (_ :*: r) = proj' (P :: Proxy ('Found pos)) r

-- p is a pair (f1, f2) scattered across q — tuple variant
instance ( Proj ('Found p1) f1 g
         , Proj ('Found p2) f2 g
         )
    => Proj ('Found ('Sum p1 p2)) (f1, f2) g where
  proj' _ x =
    ( proj' (P :: Proxy ('Found p1)) x
    , proj' (P :: Proxy ('Found p2)) x
    )

-- p is a pair (f1 :*: f2) scattered across q — (:*:) variant
instance ( Proj ('Found p1) f1 g
         , Proj ('Found p2) f2 g
         )
    => Proj ('Found ('Sum p1 p2)) (f1 :*: f2) g where
  proj' _ x =
    proj' (P :: Proxy ('Found p1)) x
    :*:
    proj' (P :: Proxy ('Found p2)) x

-- ---------------------------------------------------------------------------
-- 3.  Public constraint and projection function (replaces :< / pr)
-- ---------------------------------------------------------------------------

-- | @p :\<~ q@ means @p@ is a uniquely-occurring component of @q@,
-- where @q@ may be built from either @(,)@ or @(':*:')@ products.
type p :<~ q = Proj (ComprEmb (Elem p q)) p q

-- | Project component @p@ out of product @q@.
--   Generalises 'Data.Comp.Projection.pr' to also handle @(':*:')@.
pr :: forall p q. (p :<~ q) => q -> p
pr = proj' (P :: Proxy (ComprEmb (Elem p q)))

-- ---------------------------------------------------------------------------
-- 4.  Remainder type family
--
-- Remainder e p q  is the type of q with the component at position e
-- removed.  The result type depends on which product constructor was
-- used at each node, so we need cases for both (,) and (:*:).
-- ---------------------------------------------------------------------------

type family Remainder (e :: Emb) (p :: Type) (q :: Type) :: Type where

  -- Base: p was the whole product
  Remainder ('Found 'Here) p p = ()

  -- p was in the left branch of a tuple
  Remainder ('Found ('Le pos)) p (g, g') =
      (Remainder ('Found pos) p g, g')

  -- p was in the right branch of a tuple
  Remainder ('Found ('Ri pos)) p (g', g) =
      (g', Remainder ('Found pos) p g)

  -- p was in the left branch of a (:*:)
  Remainder ('Found ('Le pos)) p (g :*: g') =
      Remainder ('Found pos) p g :*: g'

  -- p was in the right branch of a (:*:)
  Remainder ('Found ('Ri pos)) p (g' :*: g) =
      g' :*: Remainder ('Found pos) p g

  -- p is a tuple-pair (f1, f2) scattered through q
  Remainder ('Found ('Sum pos1 pos2)) (f1, f2) q =
      Remainder ('Found pos2) f2 (Remainder ('Found pos1) f1 q)

  -- p is a (:*:)-pair (f1 :*: f2) scattered through q
  Remainder ('Found ('Sum pos1 pos2)) (f1 :*: f2) q =
      Remainder ('Found pos2) f2 (Remainder ('Found pos1) f1 q)

-- Convenience alias hiding the position evidence.
type RemainderOf p q = Remainder (ComprEmb (Elem p q)) p q

-- ---------------------------------------------------------------------------
-- 5.  ProjR — projection that also returns the remainder
-- ---------------------------------------------------------------------------

class ProjR (e :: Emb) (p :: Type) (q :: Type) where
  prR' :: Proxy e -> q -> (p, Remainder e p q)

-- Base
instance ProjR ('Found 'Here) f f where
  prR' _ x = (x, ())

-- Left branch of a tuple
instance ProjR ('Found pos) f g
    => ProjR ('Found ('Le pos)) f (g, g') where
  prR' _ (l, r) =
    let (p, remL) = prR' (P :: Proxy ('Found pos)) l
    in  (p, (remL, r))

-- Right branch of a tuple
instance ProjR ('Found pos) f g
    => ProjR ('Found ('Ri pos)) f (g', g) where
  prR' _ (l, r) =
    let (p, remR) = prR' (P :: Proxy ('Found pos)) r
    in  (p, (l, remR))

-- Left branch of a (:*:)
instance ProjR ('Found pos) f g
    => ProjR ('Found ('Le pos)) f (g :*: g') where
  prR' _ (l :*: r) =
    let (p, remL) = prR' (P :: Proxy ('Found pos)) l
    in  (p, remL :*: r)

-- Right branch of a (:*:)
instance ProjR ('Found pos) f g
    => ProjR ('Found ('Ri pos)) f (g' :*: g) where
  prR' _ (l :*: r) =
    let (p, remR) = prR' (P :: Proxy ('Found pos)) r
    in  (p, l :*: remR)

-- Scattered tuple-pair (f1, f2)
instance ( ProjR ('Found pos1) f1 q
         , ProjR ('Found pos2) f2 (Remainder ('Found pos1) f1 q)
         )
    => ProjR ('Found ('Sum pos1 pos2)) (f1, f2) q where
  prR' _ x =
    let (f1, rem1) = prR' (P :: Proxy ('Found pos1)) x
        (f2, rem2) = prR' (P :: Proxy ('Found pos2)) rem1
    in  ((f1, f2), rem2)

-- Scattered (:*:)-pair (f1 :*: f2)
instance ( ProjR ('Found pos1) f1 q
         , ProjR ('Found pos2) f2 (Remainder ('Found pos1) f1 q)
         )
    => ProjR ('Found ('Sum pos1 pos2)) (f1 :*: f2) q where
  prR' _ x =
    let (f1, rem1) = prR' (P :: Proxy ('Found pos1)) x
        (f2, rem2) = prR' (P :: Proxy ('Found pos2)) rem1
    in  (f1 :*: f2, rem2)

-- ---------------------------------------------------------------------------
-- 6.  Reconstruct — inverse of ProjR
-- ---------------------------------------------------------------------------

class Reconstruct (e :: Emb) (p :: Type) (q :: Type) where
  recons' :: Proxy e -> Remainder e p q -> p -> q

-- Base
instance Reconstruct ('Found 'Here) f f where
  recons' _ () x = x

-- Left branch of a tuple
instance Reconstruct ('Found pos) f g
    => Reconstruct ('Found ('Le pos)) f (g, g') where
  recons' _ (remL, r) p =
    (recons' (P :: Proxy ('Found pos)) remL p, r)

-- Right branch of a tuple
instance Reconstruct ('Found pos) f g
    => Reconstruct ('Found ('Ri pos)) f (g', g) where
  recons' _ (l, remR) p =
    (l, recons' (P :: Proxy ('Found pos)) remR p)

-- Left branch of a (:*:)
instance Reconstruct ('Found pos) f g
    => Reconstruct ('Found ('Le pos)) f (g :*: g') where
  recons' _ (remL :*: r) p =
    recons' (P :: Proxy ('Found pos)) remL p :*: r

-- Right branch of a (:*:)
instance Reconstruct ('Found pos) f g
    => Reconstruct ('Found ('Ri pos)) f (g' :*: g) where
  recons' _ (l :*: remR) p =
    l :*: recons' (P :: Proxy ('Found pos)) remR p

-- Scattered tuple-pair (f1, f2)
instance ( Reconstruct ('Found pos2) f2 (Remainder ('Found pos1) f1 q)
         , Reconstruct ('Found pos1) f1 q
         )
    => Reconstruct ('Found ('Sum pos1 pos2)) (f1, f2) q where
  recons' _ rem2 (f1, f2) =
    let rem1 = recons' (P :: Proxy ('Found pos2)) rem2 f2
    in  recons' (P :: Proxy ('Found pos1)) rem1 f1

-- Scattered (:*:)-pair (f1 :*: f2)
instance ( Reconstruct ('Found pos2) f2 (Remainder ('Found pos1) f1 q)
         , Reconstruct ('Found pos1) f1 q
         )
    => Reconstruct ('Found ('Sum pos1 pos2)) (f1 :*: f2) q where
  recons' _ rem2 (f1 :*: f2) =
    let rem1 = recons' (P :: Proxy ('Found pos2)) rem2 f2
    in  recons' (P :: Proxy ('Found pos1)) rem1 f1

-- ---------------------------------------------------------------------------
-- 7.  Public API
-- ---------------------------------------------------------------------------

-- | Bundles all constraints needed for 'uncons' and 'recons'.
type p :<| q =
  ( ProjR       (ComprEmb (Elem p q)) p q
  , Reconstruct (ComprEmb (Elem p q)) p q
  , p :<~ q
  )

-- | Project component @p@ from product @q@, also returning the remainder.
--
-- The remainder type is abstract — it should only be passed to 'recons'.
--
-- Law:  @uncurry (flip recons) (uncons x) == x@
--
-- Examples:
--   uncons @Int  (True, (42 :: Int, 'x'))  == (42,  (True, 'x'))
--   uncons @Bool (True :*: (42 :: Int))     == (True, () :*: 42)
uncons
  :: forall p q. (p :<| q)
  => q
  -> (p, RemainderOf p q)
uncons = prR' (P :: Proxy (ComprEmb (Elem p q)))

-- | Reconstruct the original product from a remainder and a
-- (possibly modified) projected component.
--
-- Examples:
--   let (n, rem) = uncons @Int (True, (42 :: Int, 'x'))
--   recons rem (n + 1)                    == (True, (43, 'x'))
--
--   let (b, rem) = uncons @Bool (True :*: (42 :: Int))
--   recons rem (not b)                    == (False :*: 42)
recons
  :: forall p q. (p :<| q)
  => RemainderOf p q
  -> p
  -> q
recons = recons' (P :: Proxy (ComprEmb (Elem p q)))

-- | Deconstruct and reconstruct a product, modifying a field
-- determined by the type of the given transformation function.
modify :: forall p q. (p :<| q)
  => (p -> p) -> q -> q
modify f q = recons rem (f p)
  where (p, rem) = uncons @p q

-- ---------------------------------------------------------------------------
-- 8.  Sanity checks (unexported; remove before packaging)
-- ---------------------------------------------------------------------------

-- Tuple-only product
_t1 :: (Bool, (Int, Char))
_t1 = let (n, rem) = uncons @Int (True, (42 :: Int, 'x'))
      in  recons rem (n + 1)           -- (True, (43, 'x'))

-- (:*:)-only product
_t2 :: Bool :*: Int
_t2 = let (b, rem) = uncons @Bool (True :*: (42 :: Int))
      in  recons rem (not b)           -- (False :*: 42)

-- Mixed: tuple on the outside, (:*:) on the inside
_t3 :: (Bool :*: Int, Char)
_t3 = let (n, rem) = uncons @Int ((True :*: (42 :: Int)), 'x')
      in  recons rem (n + 1)           -- ((True :*: 43), 'x')

-- Mixed: (:*:) on the outside, tuple on the inside
_t4 :: Bool :*: Int :*: Char
_t4 = let (b, rem) = uncons @Bool (True :*: (42 :: Int) :*: 'x')
      in  recons rem (not b)           -- ((False, 42) :*: 'x')