{-# LANGUAGE DerivingVia #-} -- | -- Module : Data.HBifunctor.Associative -- Copyright : (c) Justin Le 2019 -- License : BSD3 -- -- Maintainer : justin@jle.im -- Stability : experimental -- Portability : non-portable -- -- This module provides tools for working with binary functor combinators -- that represent interpretable schemas. -- -- These are types @'HBifunctor' t@ that take two functors @f@ and @g@ and returns a new -- functor @t f g@, that "mixes together" @f@ and @g@ in some way. -- -- The high-level usage of this is -- -- @ -- 'biretract' :: 'SemigroupIn' t f => t f f ~> f -- @ -- -- which lets you fully "mix" together the two input functors. -- -- @ -- 'biretract' :: (f ':+:' f) a -> f a -- biretract :: 'Plus' f => (f ':*:' f) a -> f a -- biretract :: 'Applicative' f => 'Day' f f a -> f a -- biretract :: 'Monad' f => 'Comp' f f a -> f a -- @ -- -- See "Data.HBifunctor.Tensor" for the next stage of structure in tensors -- and moving in and out of them. module Data.HBifunctor.Associative ( -- * 'Associative' Associative(..) , assoc , disassoc -- * 'SemigroupIn' , SemigroupIn(..) , matchingNE , retractNE , interpretNE -- ** Utility , biget , bicollect , (!*!) , (!$!) , (!+!) , WrapHBF(..) , WrapNE(..) ) where import Control.Applicative import Control.Applicative.ListF import Control.Applicative.Step import Control.Monad.Freer.Church import Control.Monad.Trans.Compose import Control.Monad.Trans.Identity import Control.Natural import Control.Natural.IsoF import Data.Bifunctor.Joker import Data.Coerce import Data.Data import Data.Foldable import Data.Functor.Apply.Free import Data.Functor.Bind import Data.Functor.Classes import Data.Functor.Day (Day(..)) import Data.Functor.Identity import Data.Functor.Plus import Data.Functor.Product import Data.Functor.Sum import Data.Functor.These import Data.HBifunctor import Data.HFunctor import Data.HFunctor.Internal import Data.HFunctor.Interpret import Data.Kind import Data.List.NonEmpty (NonEmpty(..)) import GHC.Generics import qualified Data.Functor.Day as D import qualified Data.Map.NonEmpty as NEM -- | An 'HBifunctor' where it doesn't matter which binds first is -- 'Associative'. Knowing this gives us a lot of power to rearrange the -- internals of our 'HFunctor' at will. -- -- For example, for the functor product: -- -- @ -- data (f ':*:' g) a = f a :*: g a -- @ -- -- We know that @f :*: (g :*: h)@ is the same as @(f :*: g) :*: h@. -- -- Formally, we can say that @t@ enriches a the category of -- endofunctors with semigroup strcture: it turns our endofunctor category -- into a "semigroupoidal category". -- -- Different instances of @t@ each enrich the endofunctor category in -- different ways, giving a different semigroupoidal category. class (HBifunctor t, Inject (NonEmptyBy t)) => Associative t where -- | The "semigroup functor combinator" generated by @t@. -- -- A value of type @NonEmptyBy t f a@ is /equivalent/ to one of: -- -- * @f a@ -- * @t f f a@ -- * @t f (t f f) a@ -- * @t f (t f (t f f)) a@ -- * @t f (t f (t f (t f f))) a@ -- * .. etc -- -- For example, for ':*:', we have 'NonEmptyF'. This is because: -- -- @ -- x ~ 'NonEmptyF' (x ':|' []) ~ 'inject' x -- x ':*:' y ~ NonEmptyF (x :| [y]) ~ 'toNonEmptyBy' (x :*: y) -- x :*: y :*: z ~ NonEmptyF (x :| [y,z]) -- -- etc. -- @ -- -- You can create an "singleton" one with 'inject', or else one from -- a single @t f f@ with 'toNonEmptyBy'. -- -- See 'Data.HBifunctor.Tensor.ListBy' for a "possibly empty" version -- of this type. type NonEmptyBy t :: (Type -> Type) -> Type -> Type -- | The isomorphism between @t f (t g h) a@ and @t (t f g) h a@. To -- use this isomorphism, see 'assoc' and 'disassoc'. associating :: (Functor f, Functor g, Functor h) => t f (t g h) <~> t (t f g) h -- | If a @'NonEmptyBy' t f@ represents multiple applications of @t f@ to -- itself, then we can also "append" two @'NonEmptyBy' t f@s applied to -- themselves into one giant @'NonEmptyBy' t f@ containing all of the @t f@s. -- -- Note that this essentially gives an instance for @'SemigroupIn' -- t (NonEmptyBy t f)@, for any functor @f@. appendNE :: t (NonEmptyBy t f) (NonEmptyBy t f) ~> NonEmptyBy t f -- | If a @'NonEmptyBy' t f@ represents multiple applications of @t f@ -- to itself, then we can split it based on whether or not it is just -- a single @f@ or at least one top-level application of @t f@. -- -- Note that you can recursively "unroll" a 'NonEmptyBy' completely -- into a 'Data.HFunctor.Chain.Chain1' by using -- 'Data.HFunctor.Chain.unrollNE'. matchNE :: Functor f => NonEmptyBy t f ~> f :+: t f (NonEmptyBy t f) -- | Prepend an application of @t f@ to the front of a @'NonEmptyBy' t f@. consNE :: t f (NonEmptyBy t f) ~> NonEmptyBy t f consNE = appendNE . hleft inject -- | Embed a direct application of @f@ to itself into a @'NonEmptyBy' t f@. toNonEmptyBy :: t f f ~> NonEmptyBy t f toNonEmptyBy = consNE . hright inject {-# MINIMAL associating, appendNE, matchNE #-} -- | Reassociate an application of @t@. assoc :: (Associative t, Functor f, Functor g, Functor h) => t f (t g h) ~> t (t f g) h assoc = viewF associating -- | Reassociate an application of @t@. disassoc :: (Associative t, Functor f, Functor g, Functor h) => t (t f g) h ~> t f (t g h) disassoc = reviewF associating -- | For different @'Associative' t@, we have functors @f@ that we can -- "squash", using 'biretract': -- -- @ -- t f f ~> f -- @ -- -- This gives us the ability to squash applications of @t@. -- -- Formally, if we have @'Associative' t@, we are enriching the category of -- endofunctors with semigroup structure, turning it into a semigroupoidal -- category. Different choices of @t@ give different semigroupoidal -- categories. -- -- A functor @f@ is known as a "semigroup in the (semigroupoidal) category -- of endofunctors on @t@" if we can 'biretract': -- -- @ -- t f f ~> f -- @ -- -- This gives us a few interesting results in category theory, which you -- can stil reading about if you don't care: -- -- * /All/ functors are semigroups in the semigroupoidal category -- on ':+:' -- * The class of functors that are semigroups in the semigroupoidal -- category on ':*:' is exactly the functors that are instances of -- 'Alt'. -- * The class of functors that are semigroups in the semigroupoidal -- category on 'Day' is exactly the functors that are instances of -- 'Apply'. -- * The class of functors that are semigroups in the semigroupoidal -- category on 'Comp' is exactly the functors that are instances of -- 'Bind'. -- -- Note that instances of this class are /intended/ to be written with @t@ -- as a fixed type constructor, and @f@ to be allowed to vary freely: -- -- @ -- instance Bind f => SemigroupIn Comp f -- @ -- -- Any other sort of instance and it's easy to run into problems with type -- inference. If you want to write an instance that's "polymorphic" on -- tensor choice, use the 'WrapHBF' newtype wrapper over a type variable, -- where the second argument also uses a type constructor: -- -- @ -- instance SemigroupIn (WrapHBF t) (MyFunctor t i) -- @ -- -- This will prevent problems with overloaded instances. class Associative t => SemigroupIn t f where -- | The 'HBifunctor' analogy of 'retract'. It retracts /both/ @f@s -- into a single @f@, effectively fully mixing them together. -- -- This function makes @f@ a semigroup in the category of endofunctors -- with respect to tensor @t@. biretract :: t f f ~> f default biretract :: Interpret (NonEmptyBy t) f => t f f ~> f biretract = retract . consNE . hright inject -- | The 'HBifunctor' analogy of 'interpret'. It takes two -- interpreting functions, and mixes them together into a target -- functor @h@. -- -- Note that this is useful in the poly-kinded case, but it is not possible -- to define generically for all 'SemigroupIn' because it only is defined -- for @Type -> Type@ inputes. See '!+!' for a version that is poly-kinded -- for ':+:' in specific. binterpret :: g ~> f -> h ~> f -> t g h ~> f default binterpret :: Interpret (NonEmptyBy t) f => (g ~> f) -> (h ~> f) -> t g h ~> f binterpret f g = retract . toNonEmptyBy . hbimap f g -- | An implementation of 'retract' that works for any instance of -- @'SemigroupIn' t@ for @'NonEmptyBy' t@. -- -- Can be useful as a default implementation if you already have -- 'SemigroupIn' implemented. retractNE :: forall t f. (SemigroupIn t f, Functor f) => NonEmptyBy t f ~> f retractNE = (id !*! biretract @t . hright (retractNE @t)) . matchNE @t -- | An implementation of 'interpret' that works for any instance of -- @'SemigroupIn' t@ for @'NonEmptyBy' t@. -- -- Can be useful as a default implementation if you already have -- 'SemigroupIn' implemented. interpretNE :: forall t g f. (SemigroupIn t f, Functor f) => (g ~> f) -> NonEmptyBy t g ~> f interpretNE f = retractNE @t . hmap f -- | An @'NonEmptyBy' t f@ represents the successive application of @t@ to @f@, -- over and over again. So, that means that an @'NonEmptyBy' t f@ must either be -- a single @f@, or an @t f (NonEmptyBy t f)@. -- -- 'matchingNE' states that these two are isomorphic. Use 'matchNE' and -- @'inject' '!*!' 'consNE'@ to convert between one and the other. matchingNE :: (Associative t, Functor f) => NonEmptyBy t f <~> f :+: t f (NonEmptyBy t f) matchingNE = isoF matchNE (inject !*! consNE) -- | Useful wrapper over 'binterpret' to allow you to directly extract -- a value @b@ out of the @t f a@, if you can convert @f x@ into @b@. -- -- Note that depending on the constraints on @f@ in @'SemigroupIn' t f@, -- you may have extra constraints on @b@. -- -- * If @f@ is unconstrained, there are no constraints on @b@ -- * If @f@ must be 'Apply', @b@ needs to be an instance of 'Semigroup' -- * If @f@ must be 'Applicative', @b@ needs to be an instance of 'Monoid' -- -- For some constraints (like 'Monad'), this will not be usable. -- -- @ -- -- Return the length of either the list, or the Map, depending on which -- -- one s in the '+' -- 'biget' 'length' length -- :: ([] :+: 'Data.Map.Map' 'Int') 'Char' -- -> Int -- -- -- Return the length of both the list and the map, added together -- 'biget' ('Data.Monoid.Sum' . length) (Sum . length) -- :: 'Day' [] (Map Int) Char -- -> Sum Int -- @ biget :: SemigroupIn t (Const b) => (forall x. f x -> b) -> (forall x. g x -> b) -> t f g a -> b biget f g = getConst . binterpret (Const . f) (Const . g) -- | Infix alias for 'biget' -- -- @ -- -- Return the length of either the list, or the Map, depending on which -- -- one s in the '+' -- 'length' '!$!' length -- :: ([] :+: 'Data.Map.Map' 'Int') 'Char' -- -> Int -- -- -- Return the length of both the list and the map, added together -- 'Data.Monoid.Sum' . length !$! Sum . length -- :: 'Day' [] (Map Int) Char -- -> Sum Int -- @ (!$!) :: SemigroupIn t (Const b) => (forall x. f x -> b) -> (forall x. g x -> b) -> t f g a -> b (!$!) = biget infixr 5 !$! -- | Infix alias for 'binterpret' -- -- Note that this is useful in the poly-kinded case, but it is not possible -- to define generically for all 'SemigroupIn' because it only is defined -- for @Type -> Type@ inputes. See '!+!' for a version that is poly-kinded -- for ':+:' in specific. (!*!) :: SemigroupIn t h => (f ~> h) -> (g ~> h) -> t f g ~> h (!*!) = binterpret infixr 5 !*! -- | A version of '!*!' specifically for ':+:' that is poly-kinded (!+!) :: (f ~> h) -> (g ~> h) -> (f :+: g) ~> h (!+!) f g = \case L1 x -> f x R1 y -> g y infixr 5 !+! -- | Useful wrapper over 'biget' to allow you to collect a @b@ from all -- instances of @f@ and @g@ inside a @t f g a@. -- -- This will work if the constraint on @f@ for @'SemigroupIn' t f@ is -- 'Apply' or 'Applicative', or if it is unconstrained. bicollect :: SemigroupIn t (Const [b]) => (forall x. f x -> b) -> (forall x. g x -> b) -> t f g a -> [b] bicollect f g = biget ((:[]) . f) ((:[]) . g) instance Associative (:*:) where type NonEmptyBy (:*:) = NonEmptyF associating = isoF to_ from_ where to_ (x :*: (y :*: z)) = (x :*: y) :*: z from_ ((x :*: y) :*: z) = x :*: (y :*: z) appendNE (NonEmptyF xs :*: NonEmptyF ys) = NonEmptyF (xs <> ys) matchNE x = case ys of L1 ~Proxy -> L1 y R1 zs -> R1 $ y :*: zs where y :*: ys = fromListF `hright` nonEmptyProd x consNE (x :*: NonEmptyF xs) = NonEmptyF $ x :| toList xs toNonEmptyBy (x :*: y ) = NonEmptyF $ x :| [y] -- | Instances of 'Alt' are semigroups in the semigroupoidal category on -- ':*:'. instance Alt f => SemigroupIn (:*:) f where biretract (x :*: y) = x y binterpret f g (x :*: y) = f x g y instance Associative Product where type NonEmptyBy Product = NonEmptyF associating = isoF to_ from_ where to_ (Pair x (Pair y z)) = Pair (Pair x y) z from_ (Pair (Pair x y) z) = Pair x (Pair y z) appendNE (NonEmptyF xs `Pair` NonEmptyF ys) = NonEmptyF (xs <> ys) matchNE x = case ys of L1 ~Proxy -> L1 y R1 zs -> R1 $ Pair y zs where y :*: ys = fromListF `hright` nonEmptyProd x consNE (x `Pair` NonEmptyF xs) = NonEmptyF $ x :| toList xs toNonEmptyBy (x `Pair` y ) = NonEmptyF $ x :| [y] -- | Instances of 'Alt' are semigroups in the semigroupoidal category on -- 'Product'. instance Alt f => SemigroupIn Product f where biretract (Pair x y) = x y binterpret f g (Pair x y) = f x g y instance Associative Day where type NonEmptyBy Day = Ap1 associating = isoF D.assoc D.disassoc appendNE (Day x y z) = z <$> x <.> y matchNE a = case fromAp `hright` ap1Day a of Day x y z -> case y of L1 (Identity y') -> L1 $ (`z` y') <$> x R1 ys -> R1 $ Day x ys z consNE (Day x y z) = Ap1 x $ flip z <$> toAp y toNonEmptyBy (Day x y z) = z <$> inject x <.> inject y -- | Instances of 'Apply' are semigroups in the semigroupoidal category on -- 'Day'. instance Apply f => SemigroupIn Day f where biretract (Day x y z) = z <$> x <.> y binterpret f g (Day x y z) = z <$> f x <.> g y instance Associative (:+:) where type NonEmptyBy (:+:) = Step associating = isoF to_ from_ where to_ = \case L1 x -> L1 (L1 x) R1 (L1 y) -> L1 (R1 y) R1 (R1 z) -> R1 z from_ = \case L1 (L1 x) -> L1 x L1 (R1 y) -> R1 (L1 y) R1 z -> R1 (R1 z) appendNE = \case L1 (Step i x) -> Step (i + 1) x R1 (Step i y) -> Step (i + 2) y matchNE = hright stepDown . stepDown consNE = stepUp . R1 . stepUp toNonEmptyBy = \case L1 x -> Step 1 x R1 y -> Step 2 y -- | All functors are semigroups in the semigroupoidal category on ':+:'. instance SemigroupIn (:+:) f where biretract = \case L1 x -> x R1 y -> y binterpret f g = \case L1 x -> f x R1 y -> g y instance Associative Sum where type NonEmptyBy Sum = Step associating = isoF to_ from_ where to_ = \case InL x -> InL (InL x) InR (InL y) -> InL (InR y) InR (InR z) -> InR z from_ = \case InL (InL x) -> InL x InL (InR y) -> InR (InL y) InR z -> InR (InR z) appendNE = \case InL (Step i x) -> Step (i + 1) x InR (Step i y) -> Step (i + 2) y matchNE = hright (viewF sumSum . stepDown) . stepDown consNE = stepUp . R1 . stepUp . reviewF sumSum toNonEmptyBy = \case InL x -> Step 1 x InR y -> Step 2 y -- | All functors are semigroups in the semigroupoidal category on 'Sum'. instance SemigroupIn Sum f where biretract = \case InR x -> x InL y -> y binterpret f g = \case InL x -> f x InR y -> g y -- | Ideally here 'NonEmptyBy' would be equivalent to 'Data.HBifunctor.Tensor.ListBy', -- just like for ':+:'. This should be possible if we can write -- a bijection. This bijection should be possible in theory --- but it has -- not yet been implemented. instance Associative These1 where type NonEmptyBy These1 = ComposeT Flagged Steps associating = isoF to_ from_ where to_ = \case This1 x -> This1 (This1 x ) That1 (This1 y ) -> This1 (That1 y) That1 (That1 z) -> That1 z That1 (These1 y z) -> These1 (That1 y) z These1 x (This1 y ) -> This1 (These1 x y) These1 x (That1 z) -> These1 (This1 x ) z These1 x (These1 y z) -> These1 (These1 x y) z from_ = \case This1 (This1 x ) -> This1 x This1 (That1 y) -> That1 (This1 y ) This1 (These1 x y) -> These1 x (This1 y ) That1 z -> That1 (That1 z) These1 (This1 x ) z -> These1 x (That1 z) These1 (That1 y) z -> That1 (These1 y z) These1 (These1 x y) z -> These1 x (These1 y z) appendNE s = ComposeT $ case s of This1 (ComposeT (Flagged _ q)) -> Flagged True q That1 (ComposeT (Flagged b q)) -> Flagged b (stepsUp (That1 q)) These1 (ComposeT (Flagged a q)) (ComposeT (Flagged b r)) -> Flagged (a || b) (q <> r) matchNE (ComposeT (Flagged isImpure q)) = case stepsDown q of This1 x | isImpure -> R1 $ This1 x | otherwise -> L1 x That1 y -> R1 . That1 . ComposeT $ Flagged isImpure y These1 x y -> R1 . These1 x . ComposeT $ Flagged isImpure y consNE s = ComposeT $ case s of This1 x -> Flagged True (inject x) That1 (ComposeT (Flagged b y)) -> Flagged b (stepsUp (That1 y)) These1 x (ComposeT (Flagged b y)) -> Flagged b (stepsUp (These1 x y)) toNonEmptyBy s = ComposeT $ case s of This1 x -> Flagged True . Steps $ NEM.singleton 0 x That1 y -> Flagged False . Steps $ NEM.singleton 1 y These1 x y -> Flagged False . Steps $ NEM.fromDistinctAscList $ (0, x) :| [(1, y)] instance Alt f => SemigroupIn These1 f where biretract = \case This1 x -> x That1 y -> y These1 x y -> x y binterpret f g = \case This1 x -> f x That1 y -> g y These1 x y -> f x g y instance Associative Void3 where type NonEmptyBy Void3 = IdentityT associating = isoF coerce coerce appendNE = \case {} matchNE = L1 . runIdentityT consNE = \case {} toNonEmptyBy = \case {} -- | All functors are semigroups in the semigroupoidal category on 'Void3'. instance SemigroupIn Void3 f where biretract = \case {} binterpret _ _ = \case {} instance Associative Comp where type NonEmptyBy Comp = Free1 associating = isoF to_ from_ where to_ (x :>>= y) = (x :>>= (unComp . y)) :>>= id from_ ((x :>>= y) :>>= z) = x :>>= ((:>>= z) . y) appendNE (x :>>= y) = x >>- y matchNE = matchFree1 consNE (x :>>= y) = liftFree1 x >>- y toNonEmptyBy (x :>>= g) = liftFree1 x >>- inject . g -- | Instances of 'Bind' are semigroups in the semigroupoidal category on -- 'Comp'. instance Bind f => SemigroupIn Comp f where biretract (x :>>= y) = x >>- y binterpret f g (x :>>= y) = f x >>- (g . y) ---- data TC f a = TCA (f a) Bool ---- | TCB (Maybe (f a)) (TC f a) -- -- sparse, non-empty list -- -- and the last item has a Bool -- -- aka sparse non-empty list tagged with a bool instance Associative Joker where type NonEmptyBy Joker = Flagged associating = isoF (Joker . Joker . runJoker) (Joker . runJoker . runJoker) appendNE (Joker (Flagged _ x)) = Flagged True x matchNE (Flagged False x) = L1 x matchNE (Flagged True x) = R1 $ Joker x instance SemigroupIn Joker f where biretract = runJoker binterpret f _ = f . runJoker instance Associative LeftF where type NonEmptyBy LeftF = Flagged associating = isoF (LeftF . LeftF . runLeftF) (LeftF . runLeftF . runLeftF) appendNE = hbind (Flagged True) . runLeftF matchNE (Flagged False x) = L1 x matchNE (Flagged True x) = R1 $ LeftF x consNE = Flagged True . runLeftF toNonEmptyBy = Flagged True . runLeftF instance SemigroupIn LeftF f where biretract = runLeftF binterpret f _ = f . runLeftF instance Associative RightF where type NonEmptyBy RightF = Step associating = isoF (RightF . runRightF . runRightF) (RightF . RightF . runRightF) appendNE = stepUp . R1 . runRightF matchNE = hright RightF . stepDown consNE = stepUp . R1 . runRightF toNonEmptyBy = Step 1 . runRightF instance SemigroupIn RightF f where biretract = runRightF binterpret _ g = g . runRightF -- | A newtype wrapper meant to be used to define polymorphic 'SemigroupIn' -- instances. See documentation for 'SemigroupIn' for more information. -- -- Please do not ever define an instance of 'SemigroupIn' "naked" on the -- second parameter: -- -- @ -- instance SemigroupIn (WrapHBF t) f -- @ -- -- As that would globally ruin everything using 'WrapHBF'. newtype WrapHBF t f g a = WrapHBF { unwrapHBF :: t f g a } deriving (Show, Read, Eq, Ord, Functor, Foldable, Traversable, Typeable, Generic, Data) instance Show1 (t f g) => Show1 (WrapHBF t f g) where liftShowsPrec sp sl d (WrapHBF x) = showsUnaryWith (liftShowsPrec sp sl) "WrapHBF" d x instance Eq1 (t f g) => Eq1 (WrapHBF t f g) where liftEq eq (WrapHBF x) (WrapHBF y) = liftEq eq x y instance Ord1 (t f g) => Ord1 (WrapHBF t f g) where liftCompare c (WrapHBF x) (WrapHBF y) = liftCompare c x y instance HBifunctor t => HBifunctor (WrapHBF t) where hbimap f g (WrapHBF x) = WrapHBF (hbimap f g x) hleft f (WrapHBF x) = WrapHBF (hleft f x) hright g (WrapHBF x) = WrapHBF (hright g x) deriving via (WrappedHBifunctor (WrapHBF t) f) instance HBifunctor t => HFunctor (WrapHBF t f) instance Associative t => Associative (WrapHBF t) where type NonEmptyBy (WrapHBF t) = NonEmptyBy t associating = isoF (hright unwrapHBF . unwrapHBF) (WrapHBF . hright WrapHBF) . associating @t . isoF (WrapHBF . hleft WrapHBF) (hleft unwrapHBF . unwrapHBF) appendNE = appendNE . unwrapHBF matchNE = hright WrapHBF . matchNE consNE = consNE . unwrapHBF toNonEmptyBy = toNonEmptyBy . unwrapHBF -- | Any @'NonEmptyBy' t f@ is a @'SemigroupIn' t@ if we have -- @'Associative' t@. This newtype wrapper witnesses that fact. We require -- a newtype wrapper to avoid overlapping instances. newtype WrapNE t f a = WrapNE { unwrapNE :: NonEmptyBy t f a } instance Associative t => SemigroupIn (WrapHBF t) (WrapNE t f) where biretract = WrapNE . appendNE . hbimap unwrapNE unwrapNE . unwrapHBF binterpret f g = biretract . hbimap f g