Copyright | © 2025 Ryan Hendrickson |
---|---|
License | BSD-3-Clause |
Safe Haskell | None |
Language | GHC2021 |
Data.CoerceSubst
Description
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.
Synopsis
- coerceSubst ∷ ∀ (σ ∷ [Subst]) a b. (Substitute σ a b, Coercible a b) ⇒ a → b
- type (↦) = '(:\)
- data (x0 ∷ k) :\ (x1 ∷ k) ∷ Subst
- type (:/) (a ∷ k) (b ∷ k) = b ↦ a
- type (↦!) = '(:\!)
- data (x0 ∷ k) :\! (x1 ∷ k) ∷ Subst
- type (:/!) (a ∷ k) (b ∷ k) = b ↦! a
- type To (_con ∷ a → b) = a ↦ b
- type Un (_con ∷ a → b) = b ↦ a
- type ToI (_con ∷ a → b) = a ↦! b
- type UnI (_con ∷ a → b) = b ↦! a
- type To_ (n ∷ Natural) (_con ∷ a → b) ∷ Subst
- type Un_ (n ∷ Natural) (_con ∷ a → b) ∷ Subst
- type ToI_ (n ∷ Natural) (_con ∷ a → b) ∷ Subst
- type UnI_ (n ∷ Natural) (_con ∷ a → b) ∷ Subst
- data Within (x0 ∷ k) (x1 ∷ [Subst]) ∷ Subst
- unsafeCoerceSubst ∷ ∀ (σ ∷ [Subst]) a b. Substitute σ a b ⇒ a → b
- data Subst
- type Substitute (σ ∷ [Subst]) (a ∷ k) (b ∷ k) ∷ Constraint
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
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] (>>=)
>>>
:}
Reference
coerceSubst ∷ ∀ (σ ∷ [Subst]) a b. (Substitute σ a b, Coercible a b) ⇒ a → b Source #
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
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\)).:/
originaloriginal
mirrors the above to match the left-to-right order of:\
replacement↦
.
>>>
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.)
data (x0 ∷ k) :\ (x1 ∷ k) ∷ Subst infix 6 Source #
An alternate spelling of ↦
, representing a substitution replacing
the left operand with the right.
type (:/) (a ∷ k) (b ∷ k) = b ↦ a infix 6 Source #
A reversed spelling of ↦
, representing a substitution replacing the
right operand with the left.
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.
type (↦!) = '(:\!) infix 6 Source #
A simple injective substitution, replacing the left operand with the right.
data (x0 ∷ k) :\! (x1 ∷ k) ∷ Subst infix 6 Source #
An alternate spelling of ↦!
, representing an injective substitution
replacing the left operand with the right.
type (:/!) (a ∷ k) (b ∷ k) = b ↦! a infix 6 Source #
A reversed spelling of ↦!
, representing a injective substitution
replacing the right operand with the left.
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.)
Advanced usage
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
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
,
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, String
is a
specific kind of []
that we don't want to become a
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.)
To
and Un
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
, as an example we have seen several times already).
However, you may not want to write out ↦
Down Bool(Int, String, Bool)
in full, with a complex type repeated unnecessarily. Some
newtypes use their type parameters multiple times, increasing the
duplication.↦
Down (Int,
String, Bool)
For such cases, this module offers the following convenience types.
type To (_con ∷ a → b) = a ↦ b Source #
As a shorthand for the common case of substituting a type with a newtype
that wraps it, you can write
. The argument to To
'MkNewtypeTo
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)
, but may look less natural. The trade-off is yours to make.↦
Down
(String, Bool)
type Un (_con ∷ a → b) = b ↦ a Source #
Like To
, but for unwrapping a newtype.
>>>
coerceSubst @'[Un ('Down @(String, Bool))] (Down ("a", True), Down ("b", False))
(("a",True),("b",False))
The above conveniences work for 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 mWrapFunctor
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 Type
-kinded case, this is exactly equivalent to m
, and in such a simple case one is likely to prefer to
write ↦!
WrapFunctor mm
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.)
Within
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))
data Within (x0 ∷ k) (x1 ∷ [Subst]) ∷ Subst Source #
A list of substitutions scoped to take effect only within arguments to the provided type expression.
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
substitution, the ↦
Const Bool IntBool
substitution takes precedence inside the pair, while the
↦
Down BoolBool
has an effect outside of it.↦
Const Bool Int
>>>
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]),())
Unsafe
unsafeCoerceSubst ∷ ∀ (σ ∷ [Subst]) a b. Substitute σ a b ⇒ a → b Source #
Just like coerceSubst
, but without the compiler-checked guarantee that
the argument and result types are safely coercible.
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
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]
Auxiliary types
type Substitute (σ ∷ [Subst]) (a ∷ k) (b ∷ k) ∷ Constraint Source #
An empty class witnessing a substitution relationship between two types.