nthese-0.1.0.1: src/Data/NThese.hs
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE UndecidableSuperClasses #-}
module Data.NThese where
-- base
import Data.Function ((&))
import Data.Functor ((<&>))
import Data.Kind (Constraint, Type)
import Data.List.NonEmpty (NonEmpty (..))
import Data.List.NonEmpty qualified as NonEmpty
import Unsafe.Coerce (unsafeCoerce)
-- sop-core
import Data.SOP (All, AllN, AllZip, AllZipN, CollapseTo, HAp (..), HCollapse (..), HExpand (..), HSequence (..), HTrans, HTraverse_ (..), K (..), NP (..), NS (..), Prod, SList (..), SListI, hmap, sList, unK, type (-.->) (apFn), type (:.:) (..))
import Data.SOP.Classes (HPure (..), HTrans (..), Same)
import Data.SOP.Constraint (SListIN, Tail)
-- these
import Data.Functor.These (These1 (..))
import Data.These (These (..))
import Data.These qualified as These
-- semialign
import Data.Align (Align (..), Semialign (..), Unalign (..))
-- * 'NThese'
{- | An n-ary generalisation of the 'These' datatype.
@'NThese' f [a1, a2, a3, ...]@ contains at least one of @f a1@, @f a2@, and so on,
and potentially all of them.
@'NThese' 'Identity' [a, b]@ is isomorphic to @'These' a b@.
= Relations to other types
* @'NP' f as@ is the n-ary product. It contains exactly @n@ elements. There is an injection from 'NP' into 'NThese' with all positions filled, but in general 'NThese' needn't fill every position.
* @'NP' ('Maybe' :.: f) as@ can contain 0 to @n@ elements, whereas 'NThese' always contains at least one element.
* @'NS' f as@ is the n-ary sum. It contains exacty 1 element. There is an injection from 'NS' into 'NThese', but in general 'NThese' can fill more positions.
* @'NonEmpty' a@ is the nonempty homogeneous list. It is similar to @'NThese' ('K' a) as@, but 'NonEmpty' can contain 1 to infinitely many elements,
while 'NThese' has at most @n@ elements and contains positional information about the present values.
-}
data NThese :: (k -> Type) -> [k] -> Type where
-- | There is a value right here, and no values in the tail. Generalises 'This'.
ThisHere ::
-- | The value right here
f a ->
NThese f (a : as)
-- | There is no value right here, but there are some values guaranteed to be in the tail. Generalises 'That'.
ThatThere ::
-- | The tail, guaranteed to contain values
NThese f as ->
NThese f (a : as)
-- | The first present value is at the head, further values are in the tail. Generalises 'These'.
TheseHere ::
-- | The first present value
f a ->
-- | The tail, guaranteed to contain further values
NThese f as ->
NThese f (a : as)
-- * Accessing the head of 'NThese'
{- | Get the first element when it's guaranteed to be present
When there is only one type variable, 'TheseHere' is the only possible constructor,
so the type is isomorphic to @f a@.
-}
unThisHere :: NThese f '[a] -> f a
unThisHere (ThisHere fa) = fa
unThisHere (TheseHere _ impossible) = case impossible of {}
unThisHere (ThatThere impossible) = case impossible of {}
-- | Extract the first element, if present.
safeHead :: NThese f (a : as) -> Maybe (f a)
safeHead (ThisHere fa) = Just fa
safeHead (TheseHere fa _) = Just fa
safeHead (ThatThere _) = Nothing
-- | Prepend an element.
cons :: f a -> NThese f as -> NThese f (a : as)
cons = TheseHere
-- | Prepend a possibly absent element.
consMaybe :: Maybe (f a) -> NThese f as -> NThese f (a : as)
consMaybe = \case
Nothing -> ThatThere
Just fa -> cons fa
-- * Relationship to 'These'
-- | Extension of 'That' to more type parameters.
mkThat :: (SListI as) => f a2 -> NThese f (a1 : a2 : as)
mkThat fa2 = ThatThere $ ThisHere fa2
-- | Extension of 'These' to more type parameters.
mkThese :: (SListI as) => f a1 -> f a2 -> NThese f (a1 : a2 : as)
mkThese fa1 fa2 = TheseHere fa1 $ ThisHere fa2
-- | Witness that 'NThese' is a generalisation of 'These' with @n >= 2@ type variables.
fromThese :: (SListI as) => These (f a1) (f a2) -> NThese f (a1 : a2 : as)
fromThese = These.these ThisHere mkThat mkThese
-- | 'NThese' is recursively isomorphic to 'These'.
toThese :: NThese f (a : as) -> These (f a) (NThese f as)
toThese = \case
ThisHere fa -> This fa
ThatThere fas -> That fas
TheseHere fa fas -> These fa fas
-- | Inverse of 'toThese'.
absorbThese :: (SListI as) => These (f a) (NThese f as) -> NThese f (a : as)
absorbThese = \case
This fa -> ThisHere fa
That fas -> ThatThere fas
These fa fas -> TheseHere fa fas
-- * Interaction with other n-ary heterogeneous datatypes
-- ** N-ary sums
{- | Injection of n-ary sums.
An n-ary sum contains exactly one element, 'NThese' contains at least one element.
-}
fromNS :: (SListI as) => NS f as -> NThese f as
fromNS = \case
Z fa -> ThisHere fa
S ns -> ThatThere $ fromNS ns
-- | Project onto the first present element, discarding the rest.
headNS :: NThese f as -> NS f as
headNS = \case
ThisHere fa -> Z fa
ThatThere fas -> S $ headNS fas
TheseHere fa _ -> Z fa
{- | Extract all elements into a 'NonEmpty' list.
The information that we have at most one element of each type is lost.
-}
toNSs :: (SListI as) => NThese f as -> NonEmpty (NS f as)
toNSs = \case
ThisHere fa -> Z fa :| []
ThatThere nt -> toNSs nt <&> S
TheseHere fa fas -> Z fa `NonEmpty.cons` fmap S (toNSs fas)
-- ** N-ary products
{- | Injection of n-ary products.
An n-ary products contains exactly, n elements, 'NThese' contains at most one element.
-}
fromNP :: (SListI as) => NP f (a : as) -> NThese f (a : as)
fromNP = \case
fa :* Nil -> ThisHere fa
fa :* fas@(_ :* _) -> TheseHere fa $ fromNP fas
{- | Injection of possibly absent n-ary products.
@'NP' ('Maybe' :.: f) as@ contains 0 to n elements, 'NThese' contains at least one element.
In case that there is no element, 'Nothing' is returned.
-}
fromNPMaybe :: NP (Maybe :.: f) as -> Maybe (NThese f as)
fromNPMaybe = \case
Nil -> Nothing
Comp (Just fa) :* fas -> Just $ maybe (ThisHere fa) (TheseHere fa) $ fromNPMaybe fas
Comp Nothing :* fas -> fromNPMaybe fas <&> ThatThere
{- | Projection onto possibly absent n-ary products.
The information that there is at least one element is lost.
-}
toNP :: (SListI as) => NThese f as -> NP (Maybe :.: f) as
toNP = \case
ThisHere fa -> Comp (Just fa) :* hpure (Comp Nothing)
ThatThere fas -> Comp Nothing :* toNP fas
TheseHere fa fas -> Comp (Just fa) :* toNP fas
{- | Zip two 'NThese' together.
Each position may contain:
* No value, this is covered by the 'NThese' structure
* One or two values, this is represented in each 'These1' structure
-}
zipNThese :: (SListI as) => NThese f as -> NThese g as -> NThese (These1 f g) as
zipNThese = \case
ThisHere fa -> \case
ThisHere ga -> ThisHere $ These1 fa ga
ThatThere gas -> TheseHere (This1 fa) $ hmap That1 gas
TheseHere ga gas -> TheseHere (These1 fa ga) $ hmap That1 gas
ThatThere fas -> \case
ThisHere ga -> TheseHere (That1 ga) $ hmap This1 fas
ThatThere gas -> ThatThere $ zipNThese fas gas
TheseHere ga gas -> TheseHere (That1 ga) $ zipNThese fas gas
TheseHere fa fas -> \case
ThisHere ga -> TheseHere (These1 fa ga) $ hmap This1 fas
ThatThere gas -> TheseHere (This1 fa) $ zipNThese fas gas
TheseHere ga gas -> TheseHere (These1 fa ga) $ zipNThese fas gas
-- * Generalisation of type classes related to 'These'
{- | Generalise 'align' to 'NThese'.
'align' has this type signature:
@
'align' :: 'Align' f => f a -> f b -> f ('These' a b)
@
This generalises zipping 2 @f@-structures, and requiring that at each @f@-position we have either an @a@ or a @b@.
'alignN' generalises this in two directions:
1. Incidental: We use nested @f (g a)@-structures. (But @g@ can always be set to identity, and is thus irrelevant.)
2. Crucial: On the input, we have @n@ values instead of 2. In the output, at each @f@-position we can have 1 to n values.
-}
alignN :: forall a as f g. (SListI as, Semialign f) => NP (f :.: g) (a : as) -> f (NThese g (a : as))
alignN = \case
Comp fga :* fgas -> case alignNP fgas of
Nothing -> ThisHere <$> fga
Just fgas' ->
align fga fgas' <&> \case
This fga' -> ThisHere fga'
That fgas'' -> ThatThere fgas''
These fga' fgas'' -> TheseHere fga' fgas''
where
alignNP :: (SListI as, Semialign f) => NP (f :.: g) as -> Maybe (f (NThese g as))
alignNP = \case
Nil -> Nothing
fgas -> case sList :: SList as of
SCons -> Just $ alignN fgas
{- | Generalise 'nil' to 'NThese'.
Creates an 'NThese' filled with 'nil's.
-}
nilN :: forall a as f g. (SListI as, Align f) => NThese (f :.: g) (a : as)
nilN = hpure $ Comp nil
{- | Generalise 'unalign' to 'NThese'.
'unalign' has this type signature:
@
'unalign' :: 'Unalign' f => f (These a b) -> (f a, f b)
@
Similar to 'alignN' we generalise this from 2 values to @n@ values.
-}
unalignN :: forall f g as. (Unalign f, SListI as) => f (NThese g as) -> NP (f :.: g) as
unalignN fgas = case sList :: SList as of
SNil -> Nil
SCons ->
fgas
& unalignWith toThese
& \(fga, fgas') -> Comp fga :* unalignN fgas'
type instance Same NThese = NThese
type instance Prod NThese = NP
-- * Helpers
-- | Helper class to constrain the type level list on 'NThese' always to be nonempty
class (SListI as) => SListINThese as
type instance SListIN NThese = SListINThese
instance (SListI as, as ~ a : as') => SListINThese as
-- | Will incur an extra constraint on the type level list not to be empty
instance HPure NThese where
hpure :: forall k (xs :: [k]) (f :: k -> Type). (SListIN NThese xs) => (forall (a :: k). f a) -> NThese f xs
hpure fa = case sList :: SList xs of
SNil -> error "Impossible pattern"
SCons -> case sList :: SList (Tail xs) of
SNil -> ThisHere fa
SCons -> TheseHere fa $ hpure fa
hcpure ::
forall k (c :: k -> Constraint) (xs :: [k]) (proxy :: (k -> Constraint) -> Type) (f :: k -> Type).
(AllN NThese c xs) =>
proxy c ->
(forall (a :: k). (c a) => f a) ->
NThese f xs
hcpure proxy fa = case sList :: SList xs of
SNil -> error "Impossible pattern"
SCons -> case sList :: SList (Tail xs) of
SNil -> ThisHere fa
SCons -> TheseHere fa $ hcpure proxy fa
instance HAp NThese where
hap = \case
Nil -> \case {}
fna :* fnas -> \case
ThisHere fa -> ThisHere $ apFn fna fa
ThatThere gas -> ThatThere $ hap fnas gas
TheseHere fa fas -> TheseHere (apFn fna fa) $ hap fnas fas
type instance CollapseTo NThese a = NonEmpty a
instance HCollapse NThese where
hcollapse = collapse_NThese
where
collapse_NThese :: (SListI as) => NThese (K a) as -> NonEmpty a
collapse_NThese = \case
ThisHere fa -> NonEmpty.singleton $ unK fa
ThatThere fas -> collapse_NThese fas
TheseHere (K fa) fas -> fa `NonEmpty.cons` collapse_NThese fas
type instance AllN NThese c = All c
instance HTraverse_ NThese where
htraverse_ ::
forall k (xs :: [k]) (g :: Type -> Type) (f :: k -> Type).
(SListIN NThese xs, Applicative g) =>
(forall (a :: k). f a -> g ()) ->
NThese f xs ->
g ()
htraverse_ f = htraverse_0
where
htraverse_0 :: (Applicative g) => NThese f as -> g ()
htraverse_0 = \case
ThisHere fa -> f fa
ThatThere fas -> htraverse_0 fas
TheseHere fa fas -> f fa *> htraverse_0 fas
hctraverse_ ::
forall k (c :: k -> Constraint) (xs :: [k]) (g :: Type -> Type) (proxy :: (k -> Constraint) -> Type) (f :: k -> Type).
(AllN NThese c xs, Applicative g) =>
proxy c ->
(forall (a :: k). (c a) => f a -> g ()) ->
NThese f xs ->
g ()
hctraverse_ _ f = hctraverse_0
where
hctraverse_0 :: (AllN NThese c as, Applicative g) => NThese f as -> g ()
hctraverse_0 = \case
ThisHere fa -> f fa
ThatThere fas -> hctraverse_0 fas
TheseHere fa fas -> f fa *> hctraverse_0 fas
instance HSequence NThese where
hsequence' = hsequence'0
where
hsequence'0 :: (Applicative f) => NThese (f :.: g) as -> f (NThese g as)
hsequence'0 = \case
ThisHere (Comp fga) -> ThisHere <$> fga
ThatThere fgas -> ThatThere <$> hsequence'0 fgas
TheseHere (Comp fga) fgas -> TheseHere <$> fga <*> hsequence'0 fgas
hctraverse' ::
forall k (c :: k -> Constraint) (xs :: [k]) (g :: Type -> Type) (proxy :: (k -> Constraint) -> Type) (f :: k -> Type) (f' :: k -> Type).
(AllN NThese c xs, Applicative g) =>
proxy c ->
(forall (a :: k). (c a) => f a -> g (f' a)) ->
NThese f xs ->
g (NThese f' xs)
hctraverse' _ f = hctraverse'0
where
hctraverse'0 :: (AllN NThese c as) => NThese f as -> g (NThese f' as)
hctraverse'0 = \case
ThisHere fa -> ThisHere <$> f fa
ThatThere fas -> ThatThere <$> hctraverse'0 fas
TheseHere fa fas -> TheseHere <$> f fa <*> hctraverse'0 fas
htraverse' ::
forall k (xs :: [k]) (g :: Type -> Type) (f :: k -> Type) (f' :: k -> Type).
(SListIN NThese xs, Applicative g) =>
(forall (a :: k). f a -> g (f' a)) ->
NThese f xs ->
g (NThese f' xs)
htraverse' f = htraverse'0
where
htraverse'0 :: NThese f as -> g (NThese f' as)
htraverse'0 = \case
ThisHere fa -> ThisHere <$> f fa
ThatThere fas -> ThatThere <$> htraverse'0 fas
TheseHere fa fas -> TheseHere <$> f fa <*> htraverse'0 fas
instance HExpand NThese where
hexpand fa0 = \case
ThisHere fa -> fa :* hpure fa0
ThatThere fas -> fa0 :* hexpand fa0 fas
TheseHere fa fas -> fa :* hexpand fa0 fas
hcexpand proxy fa0 = \case
ThisHere fa -> fa :* hcpure proxy fa0
ThatThere fas -> fa0 :* hcexpand proxy fa0 fas
TheseHere fa fas -> fa :* hcexpand proxy fa0 fas
type instance AllZipN NThese c = AllZip c
instance HTrans NThese NThese where
htrans proxy f = \case
ThisHere fa -> ThisHere $ f fa
ThatThere fas -> ThatThere $ htrans proxy f fas
TheseHere fa fas -> TheseHere (f fa) $ htrans proxy f fas
hcoerce = unsafeCoerce