{-# LANGUAGE CPP #-} {-# OPTIONS_HADDOCK redact-type-synonyms #-} -- | -- Copyright: © 2025 Ryan Hendrickson -- License: BSD-3-Clause -- -- This module exposes two functions, 'coerceSubst' and 'unsafeCoerceSubst', -- which are aliases for 'coerce' and 'unsafeCoerce' respectively. Use these -- functions to help guide type inference for the types you are coercing. -- module Data.CoerceSubst ( -- * Motivation -- $motivation -- * Reference coerceSubst, -- $basicRef type (↦), (:\), (:/), -- $injectiveIntroduction type (↦!), (:\!), (:/!), -- $injectiveRef -- ** Advanced usage -- $advancedRef -- *** @To@ and @Un@ -- $asUnIntroduction To, Un, ToI, UnI, -- $polymorphicToUn To_, Un_, ToI_, UnI_, -- *** @Within@ -- $withinIntroduction Within, -- $withinRef -- ** Unsafe unsafeCoerceSubst, -- $unsafeRef -- ** Auxiliary types Subst, Substitute, ) where import Data.Bool (Bool(..)) import Data.Coerce (Coercible, coerce) import Data.Kind (Constraint) import Data.Type.Equality (type (~)) import GHC.List (List) import GHC.TypeLits (type (-)) import Numeric.Natural (Natural) import Unsafe.Coerce (unsafeCoerce) #ifdef DOCTEST import Prelude import Control.Applicative (Const(..), ZipList(..)) import Data.Kind (Type) import Data.Ord (Down(..)) #endif infix 6 ↦, :\, :/, ↦!, :\!, :/! -- $setup -- -- >>> :set -XQuantifiedConstraints -- >>> :{ -- >>> -- (This omits features irrelevant to the demonstration.) -- >>> class Constrained (f :: Type -> Type) where -- >>> type Dom f (a :: Type) :: Constraint -- >>> type Dom f a = () -- >>> class Constrained f => CFunctor f -- >>> class CFunctor m => CBind m where -- >>> (>>-) :: (Dom m a, Dom m b) => m a -> (a -> m b) -> m b -- >>> newtype WrapFunctor f (a :: Type) = WrapFunctor {runFunctor :: f a} -- >>> instance Constrained (WrapFunctor f) where -- >>> type Dom (WrapFunctor f) a = () -- >>> instance Functor f => CFunctor (WrapFunctor f) -- >>> :} -- $motivation -- -- The type signature of 'coerce' does nothing to relate the argument and -- result types to each other. If both types are already known to the compiler, -- this is fine: -- -- > coerceMaybe ∷ Maybe A → Maybe B -- > coerceMaybe = coerce -- -- But if you are leaning on the compiler to infer one or both types, using -- 'coerce' to greatest effect can be frustrating. Take -- [this](/package/subcategories-0.2.1.1/docs/src/Control.Subcategory.Bind.html#line-37) -- example from the @subcategories@ package: -- -- > instance (Monad m) => CBind (WrapFunctor m) where -- > (>>-) :: forall a b. -- > WrapFunctor m a -- > -> (a -> WrapFunctor m b) -> WrapFunctor m b -- > (>>-) = coerce @(m a -> (a -> m b) -> m b) (>>=) -- -- All that type information is there because 'coerce' does nothing to relate -- the type of @(>>=)@ to the expected type of @(>>-)@. But the only piece of -- information that the compiler is missing is that the result of the coercion -- is the type of @(>>=)@, but with @m@ replaced by @WrapFunctor m@. -- -- 'coerceSubst' allows for that information to be encoded directly, with less -- ceremony and more clarity: -- -- >>> :{ -- >>> instance (Monad m) => CBind (WrapFunctor m) where -- >>> (>>-) = coerceSubst @'[m ↦! WrapFunctor m] (>>=) -- >>> :} -- $basicRef -- -- The first type argument to 'coerceSubst' is a list of substitutions. Simple -- substitutions can be written as @original '↦' replacement@. -- -- >>> coerceSubst @'[Bool ↦ Down Bool] (True, False, 'x') -- (Down True,Down False,'x') -- -- For the Unicode-averse, two alternate spellings are provided: -- -- * @replacement ':/' original@ is inspired by the other notation frequently -- used for substitutions (\(M[N/x]\), in which free occurrences of \(x\) -- in \(M\) are to be replaced with \(N\)). -- * @original ':\' replacement@ mirrors the above to match the left-to-right -- order of '↦'. -- -- >>> coerceSubst @'[Bool :\ Down Bool] (True, False, 'x') -- (Down True,Down False,'x') -- -- (I couldn't make up my mind which is best so you get all three.) -- $injectiveIntroduction -- -- However it is written, the effect of a substitution is to start with the -- type of the argument of 'coerceSubst', and replace every occurrence of the -- ‘original’ type with the ‘replacement’ type. The result is the type of the -- result of 'coerceSubst'. -- -- This presumes that the type of the argument is known, and the type of the -- result is to be inferred. Sometimes, the opposite is the case. For such -- cases, a substitution operator can be marked as /injective/ with a @!@ -- character. An injective substitution will additionally look for occurrences -- of the /replacement/ type in the type of the /result/, and -- reverse-substitute them into the argument type. -- $injectiveRef -- -- An example of this was shown in the Motivation section, above. Without the -- injectivity marker, GHC would not infer that the @(>>=)@ operator is to be -- instantiated at the type @m@. With it, GHC can infer this by working -- backwards from the occurrences of @WrapFunctor m@ in the expected type of -- @(>>-)@. -- -- (Not all substitutions should be injective! You may want to coerce multiple -- types to the same ‘replacement’ type, or there may be existing occurrences -- of a ‘replacement’ type in the input.) -- $advancedRef -- -- Substitutions that match a whole type take precedence over substitutions -- that match a part of a type. In this next example, see how the @Const Bool -- Int@ value is not translated by the first substitution into a @Const (Down -- Bool) Int@, because the second substitution takes precedence. -- -- >>> coerceSubst @[Bool ↦ Down Bool, Const Bool Int ↦ Bool] (True, Const @_ @Int False) -- (Down True,False) -- -- (Aside: the @'@ character that appeared in the examples before this one is -- for -- [distinguishing](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/data_kinds.html#distinguishing-between-types-and-constructors) -- between @[a]@, a list with @a@ as its single element, and @[a]@, the type of -- lists of @a@. It is optional if you compile with -- [@NoListTuplePuns@](https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/data_kinds.html#unique-syntax-for-type-level-lists-and-tuples), -- or if the list of substitutions has more than one element, as in this most -- recent example.) -- -- This behavior can be used to ‘protect’ a type from another substitution via -- an identity substitution. In this example, 'Data.String.String' is a -- specific kind of 'List' that we don't want to become a -- 'Control.Applicative.ZipList'. -- -- >>> coerceSubst @[String ↦ String, [] ↦ ZipList] ("hello", [True]) -- ("hello",ZipList {getZipList = [True]}) -- -- Once part of a type has been substituted, that part is not considered for -- any further substitutions. In other words, multiple substitutions can be -- thought of as executed in parallel, not in sequence. -- -- >>> coerceSubst @[Bool ↦ Down Bool, Down Bool ↦ Bool] (True, Down False) -- (Down True,False) -- -- If two substitutions have the same ‘original’ type, the earlier substitution -- takes precedence. -- -- >>> coerceSubst @[Bool ↦ Down Bool, Bool ↦ Const Bool Int] True -- Down True -- -- Likewise if two injective substitutions have the same ‘replacement’ type. -- -- >>> coerceSubst @[Down Bool ↦! Bool, Const Bool Int ↦! Bool] (read "Down True") ∷ Bool -- True -- >>> coerceSubst @[Down Bool ↦! Bool, Const Bool Int ↦! Bool] (read "Const True") ∷ Bool -- *** Exception: Prelude.read: no parse -- -- Order in a substitution list doesn't matter otherwise. -- -- (This is little more than a curiosity with only simple substitutions, but -- will become more relevant when 'Within' substitutions are introduced.) -- -- Finally, a known limitation: A polykinded type can't be substituted in its -- general form; it must be instantiated at each kind for which it is used. (If -- only one side of the substitution is polykinded, you won't need to worry -- about this, as the kind of '(↦)' ensures that both operands have the same -- kind.) -- $asUnIntroduction -- -- A very common case for coercions is wrapping and unwrapping newtypes. It -- isn't terribly burdensome to write these out as substitutions in some cases -- (@Bool '↦' Down Bool@, as an example we have seen several times already). -- However, you may not want to write out @(Int, String, Bool) '↦' Down (Int, -- String, Bool)@ in full, with a complex type repeated unnecessarily. Some -- newtypes use their type parameters multiple times, increasing the -- duplication. -- -- For such cases, this module offers the following convenience types. -- $polymorphicToUn -- -- The above conveniences work for 'Data.Kind.Type'-kinded substitutions. -- Function-kinded substitutions, which replace type constructors instead of -- ground types, can also be expressed with a similar shorthand. -- -- As a demonstration, the example given in the Motivation section used the -- substitution @m '↦!' WrapFunctor m@. @WrapFunctor@ is a newtype, but we -- can't use 'ToI' with this wrapping operation because @m@ and @WrapFunctor m@ -- are function-kinded. But we can use the polymorphic version, 'ToI_', -- providing (as a ‘subscript’) the number of parameters over which to be -- polymorphic. -- -- >>> :{ -- >>> instance (Monad m) => CBind (WrapFunctor m) where -- >>> (>>-) = coerceSubst @'[ToI_ 1 ('WrapFunctor @m)] (>>=) -- >>> :} -- -- As in the 'Data.Kind.Type'-kinded case, this is exactly equivalent to @m -- '↦!' WrapFunctor m@, and in such a simple case one is likely to prefer to -- write @m@ twice instead of using this more advanced (and longer!) -- ‘shorthand’. -- -- Note that while @WrapFunctor@ has two type parameters, since the last one is -- being abstracted away, only the @\@m@ needs to be written out. In general, -- if \(n\) is the subscript, you shouldn't need to explicitly instantiate the -- last \(n\) type parameters. (If you do, they are ignored.) -- $withinIntroduction -- -- In more complex scenarios, you may not want to substitute everywhere inside -- a type. The 'Within' combinator allows some substitutions to be active only -- in the arguments to a provided type constructor, which acts as a selector. -- -- >>> x = (True, Left False ∷ Either Bool Int) -- >>> coerceSubst @'[Within Either '[Bool ↦ Down Bool]] x -- (True,Left (Down False)) -- $withinRef -- -- A 'Within' combinator, when active, behaves as if expanded to its contents -- in place. For example, any substitutions that follow the 'Within' will be -- considered after the contents of the 'Within'. In this next example, because -- the 'Within' precedes the @Bool '↦' Const Bool Int@ substitution, the @Bool -- '↦' Down Bool@ substitution takes precedence inside the pair, while the -- @Bool '↦' Const Bool Int@ has an effect outside of it. -- -- >>> x = (False, ([True], [True]), [False]) -- >>> coerceSubst @[Within (,) '[Bool ↦ Down Bool], Bool ↦ Const Bool Int] x -- (Const False,([Down True],[Down True]),[Const False]) -- -- The 'Within' would not have any effect in the opposite order. -- -- >>> x = (False, ([True], [True]), [False]) -- >>> coerceSubst @[Bool ↦ Down Bool, Within (,) '[Bool ↦ Const Bool Int]] x -- (Down False,([Down True],[Down True]),[Down False]) -- -- The selector of a 'Within' can be any type-level expression, including -- compounds formed by applying a constructor to other type expressions, but -- keep in mind that the effect of the 'Within' is on the selector's arguments, -- not types in the selector itself. Notice how in this example, the selector -- @(,) Bool@ causes the payload of the 'Within' to apply to the second element -- of the pair, but not the first. -- -- >>> coerceSubst @'[Within ((,) Bool) '[Bool ↦ Down Bool]] (False, True) -- (False,Down True) -- -- The type of the pair is @(,) Bool Bool@, and only the second @Bool@ is in -- the argument of the selector @(,) Bool@. The first @Bool@ is /part/ of the -- selector. -- -- You can even use type parameters as 'Within' selectors, though in such -- circumstances you will need to ensure that the relevant 'Coercible' -- instances are available. -- -- >>> :{ -- >>> example ∷ ∀ f g a. -- >>> (∀ b b'. Coercible b b' ⇒ Coercible (f (g b)) (f (g b'))) ⇒ -- >>> Show (a, f (g (Down a))) ⇒ -- >>> (a, f (g a)) → -- >>> String -- >>> example = show . coerceSubst @'[Within g '[a ↦ Down a]] -- >>> :} -- -- 'Within' selectors are always matched against the argument type, even if the -- selector is also substituted to another type constructor. -- -- >>> coerceSubst @'[Within [] '[Bool ↦ Down Bool], [] ↦ ZipList] (False, [True]) -- (False,ZipList {getZipList = [Down True]}) -- -- This remains the case even when injective substitutions are used; the 'Within' -- matches the argument type, even though an injective substitution could cause -- inference of the argument type to be driven by the result type. -- -- >>> :{ -- >>> coerceSubst @'[Within [] '[Bool ↦! Down Bool], [] ↦! ZipList] -- >>> (read "(False, [True])") -- >>> ∷ (Bool, ZipList (Down Bool)) -- >>> :} -- (False,ZipList {getZipList = [Down True]}) -- -- 'Within's can be nested, resulting in substitutions that will take effect -- only if multiple selectors are matched in the correct order. -- -- >>> x = ([(True, True)], ([False], [False]), ()) -- >>> coerceSubst @'[Within [] '[Within (,) '[Bool ↦ Down Bool]]] x -- ([(Down True,Down True)],([False],[False]),()) -- $unsafeRef -- -- All of the above applies equally to 'unsafeCoerceSubst' (except for -- requiring 'Coercible' instances), though with this function you gain the -- ability to do less safe things, like coercing through an unknown -- 'Data.Functor.Functor'. -- -- >>> caution = unsafeCoerceSubst @'[Bool ↦ Down Bool] . fmap even -- >>> :t caution -- caution ∷ (Functor f', Integral a) ⇒ f' a → f' (Down Bool) -- >>> caution [3..5] -- not so dangerous on lists! -- [Down False,Down True,Down False] -- | The kind of an element in the type substitution DSL. -- -- To make 'Subst's, see '(↦)', '(:\)', '(:/)', '(↦!)', '(:\!)', '(:/!)', 'To', -- 'Un', 'ToI', 'UnI', 'To_', 'Un_', 'ToI_', 'UnI_', and 'Within'. type data Subst = ∀ k. k :\ k -- ^ An alternate spelling of '↦', representing a substitution replacing -- the left operand with the right. | ∀ k. k :\! k -- ^ An alternate spelling of '↦!', representing an injective substitution -- replacing the left operand with the right. | ∀ k. Within k (List Subst) -- ^ A list of substitutions scoped to take effect only within arguments to -- the provided type expression. -- | A simple substitution, replacing the left operand with the right. type (↦) ∷ ∀ k. k → k → Subst type (↦) = (:\) -- | A simple injective substitution, replacing the left operand with the -- right. type (↦!) ∷ ∀ k. k → k → Subst type (↦!) = (:\!) -- | A reversed spelling of '↦', representing a substitution replacing the -- /right/ operand with the /left/. type (:/) ∷ ∀ k. k → k → Subst type a :/ b = b ↦ a -- | A reversed spelling of '↦!', representing a injective substitution -- replacing the /right/ operand with the /left/. type (:/!) ∷ ∀ k. k → k → Subst type a :/! b = b ↦! a -- | An alias for 'coerce' that accepts a type-level list of type substitutions -- to perform. As with 'coerce', the compiler will check that the argument and -- result types are safely coercible using the 'Coercible' constraint, and the -- coercion is free of cost at run time. coerceSubst ∷ ∀ σ a b. Substitute σ a b ⇒ Coercible a b ⇒ a → b coerceSubst :: ∀ (σ ∷ [Subst]) a b. (Substitute σ a b, Coercible a b) ⇒ a → b coerceSubst = a → b ∀ a b. Coercible a b ⇒ a → b coerce {-# INLINE coerceSubst #-} -- | Just like 'coerceSubst', but without the compiler-checked guarantee that -- the argument and result types are safely coercible. unsafeCoerceSubst ∷ ∀ σ a b. Substitute σ a b ⇒ a → b unsafeCoerceSubst :: ∀ (σ ∷ [Subst]) a b. Substitute σ a b ⇒ a → b unsafeCoerceSubst = a → b ∀ a b. a → b unsafeCoerce {-# INLINE unsafeCoerceSubst #-} -- | As a shorthand for the common case of substituting a type with a newtype -- that wraps it, you can write @'To' 'MkNewtype@. The argument to 'To' should -- be the promoted data constructor for the newtype, not the newtype's type -- constructor (in many cases, these names are punned, and the @'@ is necessary -- to disambiguate them). -- -- If the constructor is polymorphic, it is necessary to instantiate all of its -- type parameters, as in this example. -- -- >>> coerceSubst @'[To ('Down @(String, Bool))] (("a", True), ("b", False)) -- (Down ("a",True),Down ("b",False)) -- -- This is a bit more concise than the equivalent @(String, Bool) '↦' Down -- (String, Bool)@, but may look less natural. The trade-off is yours to make. type To (_con ∷ a → b) = a ↦ b -- | Like 'To', but injective. type ToI (_con ∷ a → b) = a ↦! b -- $ -- >>> f x = coerceSubst @'[ToI ('Down @(String, Bool))] x ∷ (Down (String, Bool), Bool) -- >>> f $ read "((\"a\", True), False)" -- (Down ("a",True),False) -- | Like 'To', but for unwrapping a newtype. -- -- >>> coerceSubst @'[Un ('Down @(String, Bool))] (Down ("a", True), Down ("b", False)) -- (("a",True),("b",False)) type Un (_con ∷ a → b) = b ↦ a -- | Like 'Un', but injective. type UnI (_con ∷ a → b) = b ↦! a -- $ -- >>> f x = coerceSubst @'[UnI ('Down @(String, Bool))] x ∷ ((String, Bool), Bool) -- >>> f $ read "(Down (\"a\", True), False)" -- (("a",True),False) -- | A variant of 'To' for polymorphic substitution, accepting the number of -- type arguments over which to abstract. @'To_' 0 'Con@ is equivalent to @'To' -- 'Con@. type To_ n (_con ∷ a → b) = MkTo_ False n a b -- $ -- >>> coerceSubst @'[To_ 1 'ZipList] ("a", ([True], False)) -- (ZipList {getZipList = "a"},(ZipList {getZipList = [True]},False)) -- | Like 'To_', but injective. @'ToI_' 0 'Con@ is equivalent to @'ToI' 'Con@. type ToI_ n (_con ∷ a → b) = MkTo_ True n a b -- $ -- >>> f x = coerceSubst @'[ToI_ 1 'ZipList] x ∷ (ZipList Char, (ZipList Bool, Bool)) -- >>> f $ read "(\"a\", ([True], False))" -- (ZipList {getZipList = "a"},(ZipList {getZipList = [True]},False)) -- | Like 'To_', but for unwrapping a newtype. @'Un_' 0 'Con@ is equivalent to -- @'Un' 'Con@. type Un_ n (_con ∷ a → b) = MkTo_ False n b a -- $ -- >>> coerceSubst @'[Un_ 1 'ZipList] (ZipList "a", (ZipList [True], False)) -- ("a",([True],False)) -- | Like 'Un_', but injective. @'UnI_' 0 'Con@ is equivalent to @'UnI' 'Con@. type UnI_ n (_con ∷ a → b) = MkTo_ True n b a -- $ -- >>> f x = coerceSubst @'[UnI_ 1 'ZipList] x ∷ (String, ([Bool], Bool)) -- >>> f $ read "(ZipList {getZipList = \"a\"}, (ZipList {getZipList = [True]}, False))" -- ("a",([True],False)) -- | The shared implementation of polymorphic 'To_', 'ToI_', 'Un_', and 'UnI_'. type MkTo_ ∷ ∀ k. Bool → Natural → k → k → Subst type family MkTo_ inj n a b where MkTo_ False 0 a b = a ↦ b MkTo_ True 0 a b = a ↦! b MkTo_ inj n (f x) (g x) = MkTo_ inj (n - 1) f g -- What follows is hideous, and I make no apologies for it. -- -- The type-level computation needed to perform substitutions is, when -- distilled into a more familiar term-level pseudocode form, not very -- complicated. -- -- > substitute ∷ ∀ k₀. List Subst → k₀ → k₀ → Constraint -- > substitute σ₀ = go σ₀ -- > where -- > go ∷ ∀ k. List Subst → k → k → Constraint -- > go = \cases -- > (from ↦ to : _) arg res | from ~ arg → to ~ res -- > (from ↦! to : _) arg res | from ~ arg → to ~ res -- > | to ~ res → from ~ arg -- > (_ : σ) arg res → go σ arg res -- > [] (f a) res → ∃ f' a'. -- > (substitute σ₀ f f', substitute (expand σ₀ f) a a', f' a' ~ res) -- > [] arg (f b) → ∃ f' b'. -- > (substitute σ₀ f' f, substitute (expand σ₀ f') b' b, arg ~ f' b') -- > [] arg res → arg ~ res -- > -- > expand ∷ ∀ k₀. List Subst → k₀ → List Subst -- > expand (Within w_f w_σ : σ) f₀ = doWithin f₀ -- > where -- > doWithin ∷ ∀ k. k → List Subst -- > doWithin = \case -- > f | w_f ~ f → w_σ ++ σ' -- > (f _) → doWithin f -- > _ → Within w_f w_σ : σ' -- > σ' = expand σ f₀ -- > expand (s : σ) f = s : expand σ f -- > expand [] _ = [] -- -- As much as possible of the above has been translated into type families. One -- might ask, why not all of it? To which I would reply, type families have no -- ability to do incoherent matches, which would make this module unusable with -- parameterized types. There are a few places in the above where I have -- written pseudocode representing testing two types for equality, or -- decomposing a type into an @f a@ application. One can encode these matches -- with type families, but the resulting type gets stuck in cases like @IsEq a -- Char@ (@IsEq@ being the notional type-family-based equality tester), because -- the compiler will nobly insist on not telling us any convenient lies. (@a@ -- /could/ be @Char@!) -- -- So instead I am incorporating incoherent instances with functional -- dependencies, which enable a more allistic disposition on such questions as -- whether @a@ and @Char@ are the same (‘shut up nerd, they aren't’). They are -- also, sadly, much more verbose and difficult to write (not to mention read), -- so rather than encode the entire type-level algorithm in class instances, I -- have put the bulk of the pattern matching and control flow into a -- 'Constraint'-kinded closed type family named, uncreatively, 'Eval', which -- dispatches on a single 'Expr'. (‘Haskell is a dynamically-typed, interpreted -- language,’ as Vidrun helpfully teaches us.) Returning a 'Constraint' allows -- us to step outside the type family paradigm as needed by returning a real -- class, but fortunately this isn't necessary very much: only for terminal -- equalities ('(~)'), the incoherent-by-design instances ('KnownEq' and -- 'KnownApp'), and a pair of auxiliary classes representing tuples of -- constraints related by newly-introduced unknown type parameters -- ('EvalSubstituteApp' and '(:&:)', of which the latter is more often invoked -- through the '(:?:)' / '(:::)' ternary interface than directly). -- -- Note that having monolithic 'Expr' and 'Eval' instead of a number of smaller -- type families is very useful, as it enables one set of generic operators to -- be reused in different parts of the algorithm. They work by holding -- partially-applied 'Expr' values instead of attempting to partially apply a -- type family (which is illegal). -- | An empty class witnessing a substitution relationship between two types. type Substitute ∷ List Subst → k → k → Constraint type Substitute σ a b = Eval (Substitute_go σ σ a b) -- | A representation of work to be done while solving a 'Substitute' -- constraint. Some of these constructors are ad-hoc for this application, and -- some are generic combinators that save me from repeating the same pattern -- multiple times. -- -- The choice of final argument is often significant, so that it can be used -- with '(:&:)'. type data Expr -- | The worker function of 'Substitute'. = ∀ k. Substitute_go (List Subst) -- ^ initial list of substitutions (List Subst) -- ^ substitutions still to be handled k -- ^ input type of the coercion k -- ^ result type of the coercion -- | Find any matching 'Within' substitutions and expand them in place. | ∀ k. Expand (List Subst) -- ^ list of substitutions k -- ^ type to scrutinize for matching 'Within' fragments (List Subst) -- ^ expanded list -- | Helper function for expanding a single 'Within'. | ∀ k₁ k₂. Expand_doWithin (ExpandWithinEnv k₁) k₂ -- ^ type to scrutinize (List Subst) -- ^ a suffix of already-expanded substitutions to append to the result -- | Helper function for continuing a 'Within' expansion in an applied type. | ∀ k₁ k₂. Expand_doWithinApp (ExpandWithinEnv k₁) k₂ -- ^ type to scrutinize (List Subst) -- ^ a suffix of already-expanded substitutions to append to the result -- | Helper function for building a list. | ∀ k. DoCons k {-^ head -} (List k) {-^ head : tail -} (List k) {-^ tail -} -- | Generic holder for a unary 'Constraint'-kinded function. | ∀ k. Lift1 (k → Constraint) k -- | Generic holder for a pair of control branches. | (:::) Constraint {-^ if true -} Constraint {-^ if false -} Bool type ExpandWithinEnv k = ( k -- selector of the 'Within' , List Subst -- payload of the 'Within' , List Subst -- result of the expansion, starting with either this payload or -- a re-wrapped 'Within' ) -- | Constraint-kinded ternary operator: @Condition :?: IfTrue ::: IfFalse@ type (:?:) ∷ (Bool → Constraint) → (Bool → Expr) → Constraint type (:?:) cond = (:&:) (Lift1 cond) infixr 0 :?:, ::: -- | Generic operator for cutting two 'Expr's with an unknown parameter to be -- solved. A little bit like a symmetric type-level @$@ or @&@, but don't lean -- on that too hard. class (f ∷ k → Expr) :&: (g ∷ k → Expr) instance (Eval (f a), Eval (g a)) ⇒ f :&: g type family Eval (expr ∷ Expr) ∷ Constraint where Eval (Substitute_go σ₀ (from ↦ to : σ) arg res) = from `KnownEq` arg :?: to ~ res ::: Eval (Substitute_go σ₀ σ arg res) Eval (Substitute_go σ₀ (from ↦! to : σ) arg res) = from `KnownEq` arg :?: to ~ res ::: to `KnownEq` res :?: from ~ arg ::: Eval (Substitute_go σ₀ σ arg res) Eval (Substitute_go σ₀ (_ : σ) arg res) = Eval (Substitute_go σ₀ σ arg res) Eval (Substitute_go σ₀ '[] arg res) = -- Once all 'Subst's have been examined, if we can recurse into @arg@ or -- @res@, do so; otherwise @arg@ doesn't get modified. KnownApp arg :?: EvalSubstituteApp σ₀ arg res False ::: KnownApp res :?: EvalSubstituteApp σ₀ arg res True ::: arg ~ res -- Expand this 'Within' and prepend it to the result of expanding the -- remaining 'Subst's. Eval (Expand (Within w_f w_σ : σ) f₀ res_σ) = Expand_doWithin '(w_f, w_σ, res_σ) f₀ :&: Expand σ f₀ -- Non-'Within's don't get expanded, but they are preserved. Eval (Expand (s : σ) f₀ res_σ) = DoCons s res_σ :&: Expand σ f₀ Eval (Expand '[] _ res_σ) = '[] ~ res_σ Eval (Expand_doWithin '(w_f, w_σ, res_σ) f σ') = -- If this 'Within' matches the current scrutinee, prepend its payload. w_f `KnownEq` f :?: w_σ ++ σ' ~ res_σ ::: -- Otherwise, advance up the spine of the scrutinee if we can. KnownApp f :?: Eval (Expand_doWithinApp '(w_f, w_σ, res_σ) f σ') ::: -- If we can't, this 'Within' does not trigger. Preserve it as-is in the -- result. Within w_f w_σ : σ' ~ res_σ -- Exists only to decompose the scrutinee into @f _@. Generally, doing this -- in a type family would risk getting stuck on unknown cases, but this -- helper is only used guarded by 'KnownApp'. Eval (Expand_doWithinApp env (f _) σ') = Eval (Expand_doWithin env f σ') Eval (DoCons a α₁ α₂) = a : α₂ ~ α₁ Eval ((c ::: _) True) = c Eval ((_ ::: c) False) = c Eval (Lift1 f a) = f a -- | Independently substitute in the two parts of a type application and -- recombine the results. Can be driven ‘forward’ or ‘in reverse’, meaning that -- @f_a@ or @f_b@ respectively are to be used as the source of type information -- for the next substitution passes. class EvalSubstituteApp (σ ∷ List Subst) -- ^ list of substitutions (f_a ∷ k) -- ^ a type, known to be an application, and not matching anything in @σ@ (f_b ∷ k) -- ^ @f_a@ after applying @σ@, known to be an application, and not matching -- anything injective in @σ@ (in_reverse ∷ Bool) instance ( Substitute σ f f' , Eval (Expand σ f σ') , Substitute σ' a a' , f' a' ~ f_b ) ⇒ EvalSubstituteApp σ (f a) f_b False instance ( Substitute σ f' f , Eval (Expand σ f' σ') , Substitute σ' b' b , f_a ~ f' b' ) ⇒ EvalSubstituteApp σ f_a (f b) True -- | Put 'True' in @result@ if the compiler knows that the two argument types -- are equal, and 'False' if there is any doubt. For this use case, this level -- of certainty is good enough. class KnownEq (a ∷ k₁) (b ∷ k₂) (result ∷ Bool) | a b → result instance True ~ result ⇒ KnownEq a a result instance {-# INCOHERENT #-} False ~ result ⇒ KnownEq _₁ _₂ result -- | Put 'True' in @result@ if the compiler knows that the argument type is an -- application of one type to another, and 'False' if there is any doubt. For -- this use case, this level of certainty is good enough. class KnownApp (a ∷ k) (result ∷ Bool) | a → result instance True ~ result ⇒ KnownApp (f _₁) result instance {-# INCOHERENT #-} False ~ result ⇒ KnownApp _₁ result -- | Append two type-level lists. Come on, did you expect anything else? type (++) ∷ List a → List a → List a type family α₁ ++ α₂ where '[] ++ α = α (a : α₁) ++ α₂ = a : (α₁ ++ α₂)