packages feed

large-anon-0.3.4: test/Test/Infra/Discovery.hs

{-# LANGUAGE DataKinds           #-}
{-# LANGUAGE FlexibleContexts    #-}
{-# LANGUAGE GADTs               #-}
{-# LANGUAGE KindSignatures      #-}
{-# LANGUAGE PolyKinds           #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications    #-}
{-# LANGUAGE TypeOperators       #-}

module Test.Infra.Discovery (
     -- * Intersect rows
     InBothRows(..)
   , inLeftRow
   , inRightRow
   , intersectRows
     -- * Check for projection
   , NotSubRow
   , checkIsSubRow
   , maybeProject
     -- * Compute intersection
   , Intersection(..)
   , intersect
     -- * Auxiliary
   , catMaybeF
   , pairFst
   , pairSnd
   ) where

import Data.Kind
import Data.Maybe (catMaybes)
import Data.Typeable
import GHC.TypeLits

import Data.Record.Anon
import Data.Record.Anon.Advanced (Record, InRow(InRow))
import qualified Data.Record.Anon.Advanced as Anon
import Control.Monad.State

{-------------------------------------------------------------------------------
  Intersect rows

  NOTE: A constraint @SubRow r r@ is not completely trivial: it means that
  there are no shadowed fields.
-------------------------------------------------------------------------------}

data InBothRows (r1 :: Row k) (r2 :: Row k) (a :: k) where
  InBothRows :: forall k (n :: Symbol) (r1 :: Row k) (r2 :: Row k) (a :: k).
       ( KnownSymbol n
       , RowHasField n r1 a
       , RowHasField n r2 a
       )
    => Proxy n -> InBothRows r1 r2 a

inLeftRow :: InBothRows r1 r2 a -> InRow r1 a
inLeftRow (InBothRows n) = InRow n

inRightRow :: InBothRows r1 r2 a -> InRow r2 a
inRightRow (InBothRows n) = InRow n

intersectRows :: forall k (r1 :: Row k) (r2 :: Row k) proxy proxy'.
     ( KnownFields r1
     , KnownFields r2
     , SubRow r1 r1
     , SubRow r2 r2
     , AllFields r1 Typeable
     , AllFields r2 Typeable
     )
  => proxy r1 -> proxy' r2 -> Record (Maybe :.: InBothRows r1 r2) r2
intersectRows _ _ =
    go Anon.reifySubRow Anon.reifySubRow
  where
    go :: Record (InRow r1) r1
       -> Record (InRow r2) r2
       -> Record (Maybe :.: InBothRows r1 r2) r2
    go r1 r2 = Anon.cmap (Proxy @Typeable) (findField r1) r2

    findField :: forall x2.
          Typeable x2
       => Record (InRow r1) r1 -> InRow r2 x2 -> (Maybe :.: InBothRows r1 r2) x2
    findField r1 f = Comp $
        findMatch . catMaybes $
          Anon.collapse $ Anon.cmap (Proxy @Typeable) (K . checkIsMatch f) r1

    checkIsMatch :: forall x1 x2.
         (Typeable x1, Typeable x2)
      => InRow r2 x2 -> InRow r1 x1 -> Maybe (InBothRows r1 r2 x2)
    checkIsMatch (InRow x2) (InRow x1) = do
        Refl <- sameSymbol x1 x2
        Refl <- eqT :: Maybe (x1 :~: x2)
        return $ InBothRows x1

    findMatch :: [a] -> Maybe a
    findMatch []  = Nothing
    findMatch [a] = Just a
    findMatch _   = error "intersectRows: error: multiple matches"

{-------------------------------------------------------------------------------
  Check for projection
-------------------------------------------------------------------------------}

-- | Fields that are missing or have the wrong type
--
-- TODO: Ideally we should distinguish between type errors and missing fields.
type NotSubRow = [String]

checkIsSubRow :: forall k (r1 :: Row k) (r2 :: Row k) proxy proxy'.
     ( KnownFields r1
     , KnownFields r2
     , SubRow r1 r1
     , SubRow r2 r2
     , AllFields r1 Typeable
     , AllFields r2 Typeable
     )
  => proxy r1 -> proxy' r2 -> Either NotSubRow (Reflected (SubRow r1 r2))
checkIsSubRow p1 p2 =
     uncurry postprocess . flip runState [] . Anon.sequenceA $
       Anon.zipWith
         checkInLeft
         (intersectRows p1 p2)
         (Anon.reifyKnownFields (Proxy @r2))
  where
    checkInLeft ::
         (Maybe :.: InBothRows r1 r2) x
      -> K String x
      -> (State [String] :.: Maybe :.: InRow r1) x
    checkInLeft (Comp Nothing) (K name) = Comp $ state $ \missing ->
        (Comp Nothing, name : missing)
    checkInLeft (Comp (Just inBoth)) _ = Comp $ state $ \missing ->
        (Comp (Just (inLeftRow inBoth)), missing)

    postprocess ::
         Record (Maybe :.: InRow r1) r2
      -> [String]
      -> Either NotSubRow (Reflected (SubRow r1 r2))
    postprocess matched missing =
        maybe (Left missing) (Right . Anon.reflectSubRow) $
          Anon.sequenceA matched

maybeProject :: forall k (f :: k -> Type) (r1 :: Row k) (r2 :: Row k) proxy.
     ( KnownFields r1
     , KnownFields r2
     , SubRow r1  r1
     , SubRow r2 r2
     , AllFields r1  Typeable
     , AllFields r2 Typeable
     )
  => Record f r1 -> proxy r2 -> Either NotSubRow (Record f r2)
maybeProject r1 p = aux <$> checkIsSubRow r1 p
  where
    aux :: Reflected (SubRow r1 r2) -> Record f r2
    aux Reflected = Anon.project r1

{-------------------------------------------------------------------------------
  Compute intersection
-------------------------------------------------------------------------------}

data Intersection (r1 :: Row k) (r2 :: Row k) where
    Intersection :: forall k (r1 :: Row k) (r2 :: Row k) (ri :: Row k).
         ( KnownFields ri
         , SubRow r1 ri
         , SubRow r2 ri
         )
      => Proxy ri -> Intersection r1 r2

intersect :: forall k (r1 :: Row k) (r2 :: Row k) proxy proxy'.
     ( KnownFields r1
     , KnownFields r2
     , SubRow r1 r1
     , SubRow r2 r2
     , AllFields r1 Typeable
     , AllFields r2 Typeable
     )
  => proxy r1 -> proxy' r2 -> Intersection r1 r2
intersect p1 p2 =
    (\(Anon.SomeRecord r) -> aux $ Anon.map pairSnd r) $
      catMaybeF (intersectRows p1 p2)
  where
    aux :: forall ri.
         KnownFields ri
      => Record (InBothRows r1 r2) ri -> Intersection r1 r2
    aux r =
        case (project1, project2) of
          (Reflected, Reflected) -> Intersection (Proxy @ri)
      where
        project1 :: Reflected (SubRow r1 ri)
        project1 = Anon.reflectSubRow $ Anon.map inLeftRow r

        project2 :: Reflected (SubRow r2 ri)
        project2 = Anon.reflectSubRow $ Anon.map inRightRow r

{-------------------------------------------------------------------------------
  Auxiliary
-------------------------------------------------------------------------------}

catMaybeF :: KnownFields r => Record (Maybe :.: f) r -> Anon.SomeRecord f
catMaybeF =
      Anon.someRecord
    . catMaybes
    . map distrib
    . Anon.toList
    . Anon.map (K . Some)
  where
    distrib :: (String, Some (Maybe :.: f)) -> Maybe (String, Some f)
    distrib (_, Some (Comp Nothing))   = Nothing
    distrib (n, Some (Comp (Just fx))) = Just (n, Some fx)

pairFst :: Product f g x -> f x
pairFst (Pair fx _) = fx

pairSnd :: Product f g x -> g x
pairSnd (Pair _ gx) = gx