Skip to content

Commit 5e63f2d

Browse files
committed
+SWChain for chaining strongweak ops; many tweaks
The implementation of SWChain is really cool. We have to do some `unsafeCoerce`ing and use intermediate type classes, but we get fully parametric chaining. I'm very proud of it.
1 parent b21d39e commit 5e63f2d

11 files changed

+282
-95
lines changed

TODO.md

-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
# strongweak to-dos
2-
* OOPS overlap between `Weak :: Weaken a => Type` and `Weak :: Strength`
32
* split into base definitions and orphan instances?
43
* deleted generic failure tests because clumsy. kinda sad but idk :(
54

src/Strongweak.hs

+20-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import Strongweak.Strengthen
2626

2727
{- $strongweak-instance-design
2828
29-
A given strong type @a@ has exactly one associated weak type @'Weak' a@.
29+
A given strong type @a@ has exactly one associated weak type @'Weakened' a@.
3030
Multiple strong types may weaken to the same weak type.
3131
3232
The following laws must hold:
@@ -43,6 +43,12 @@ with the overall design. Here is some relevant guidance.
4343
* Most (all?) instances should handle (relax or assert) a single invariant.
4444
* Most instances should not have a recursive context.
4545
46+
If you want to handle multiple invariants, chain the weakens/strengthens.
47+
You may do this by nesting 'SW' uses, but you will then have to write your own
48+
instances, as the generics cannot handle such chaining. Alternatively, you may
49+
use 'SWChain'. This will add another newtype layer to the strong representation,
50+
but the generics are happy with it.
51+
4652
Some types may not have any invariants which may be usefully relaxed e.g.
4753
@'Either' a b@. For these, you may write a recursive instance that
4854
weakens/strengthens "through" the type e.g. @('Weak' a, 'Weak' b) => Weak
@@ -57,4 +63,17 @@ a @'Weak' a = a, weaken = id@ overlapping instance, which I do not want. On the
5763
other hand, @[a]@ /does/ weaken to @['Weak' a]@, because there are no invariants
5864
present to remove, so decomposing is all the user could hope to do.
5965
66+
Another problem is coercible newtypes such as @Tagged@ from the @tagged@
67+
package. These have no invariants for us to weaken, so one's knee-jerk reaction
68+
might be to weaken the inner type. But what if the user doesn't want to weaken
69+
that inner type? They might be happy with using 'Data.Word.Word' instead of
70+
'Numeric.Natural.Natural', for example.
71+
72+
strongweak provides a special type 'Coercibly' which permits precisely defining
73+
how to weaken a newtype/pair of coercible types. For this reason, zero-invariant
74+
coercible newtypes are excluded from the strongweak ecosystem (e.g.
75+
'Data.Functor.Identity.Identity', 'Data.Functor.Const.Const', @Tagged@).
76+
77+
If your newtype puts the wrapped type as the final type variable, use
78+
'Coercibly1' for a considerably simpler invocation.
6079
-}

src/Strongweak/Chain.hs

+113
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{-# LANGUAGE UndecidableInstances #-} -- type family 'Weakened' in constraints
2+
{-# LANGUAGE AllowAmbiguousTypes #-} -- TODO
3+
4+
module Strongweak.Chain where
5+
6+
import Strongweak.Weaken
7+
import Strongweak.Strengthen
8+
import GHC.TypeNats
9+
10+
import Unsafe.Coerce
11+
12+
{- | When weakening (or strengthening), chain the operation @n@ times.
13+
14+
You may achieve this without extra newtypes by nesting uses of
15+
'Strongweak.Weaken.SW'. However, strongweak generics can't handle this, forcing
16+
you to write manual instances.
17+
18+
'SWChain' provides this nesting behaviour in a type. In return for adding a
19+
boring newtype layer to the strong representation, you can chain weakening and
20+
strengthenings without having to write them manually.
21+
22+
The type works as follows:
23+
24+
@
25+
Weakened (SWChain 0 a) = a
26+
Weakened (SWChain 1 a) = Weakened a
27+
Weakened (SWChain 2 a) = Weakened (Weakened a)
28+
Weakened (SWChain n a) = WeakenedN n a
29+
@
30+
31+
And so on. (This type is only much use from @n = 2@ onwards.)
32+
-}
33+
newtype SWChain (n :: Natural) a = SWChain { unSWChain :: a }
34+
deriving stock Show
35+
36+
instance WeakenN n a => Weaken (SWChain n a) where
37+
type Weakened (SWChain n a) = WeakenedN n a
38+
weaken = weakenN @n @a . unSWChain
39+
40+
class WeakenN (n :: Natural) a where
41+
weakenN :: a -> WeakenedN n a
42+
43+
-- | Zero case: return the value as-is.
44+
--
45+
-- TODO The overlapping rules always confuse me. @OVERLAPPING@ is right, right?
46+
instance {-# OVERLAPPING #-} WeakenN 0 a where
47+
weakenN = id
48+
49+
-- | Inductive case. @n /= 0@, else this explodes.
50+
instance (Weaken a, WeakenN (n-1) (Weakened a)) => WeakenN n a where
51+
weakenN a =
52+
case weakenN @(n-1) @(Weakened a) (weaken a) of
53+
x -> weakenedNRL1 @n @a x
54+
55+
-- | Inverted inductive 'WeakenedN'case.
56+
--
57+
-- @n@ must not be 0.
58+
weakenedNRL1 :: forall n a. WeakenedN (n-1) (Weakened a) -> WeakenedN n a
59+
weakenedNRL1 = unsafeCoerce
60+
61+
-- | Inductive 'WeakenedN'case.
62+
--
63+
-- @n@ must not be 0.
64+
weakenedNLR1 :: forall n a. WeakenedN n a -> WeakenedN (n-1) (Weakened a)
65+
weakenedNLR1 = unsafeCoerce
66+
67+
instance StrengthenN n a => Strengthen (SWChain n a) where
68+
strengthen = fmap SWChain . strengthenN @n @a
69+
70+
class WeakenN n a => StrengthenN (n :: Natural) a where
71+
strengthenN :: WeakenedN n a -> Either StrengthenFailure' a
72+
73+
instance {-# OVERLAPPING #-} StrengthenN 0 a where
74+
strengthenN = Right
75+
76+
instance (Strengthen a, StrengthenN (n-1) (Weakened a))
77+
=> StrengthenN n a where
78+
strengthenN a =
79+
case strengthenN @(n-1) @(Weakened a) (weakenedNLR1 @n @a a) of
80+
Left e -> Left e
81+
Right sa -> strengthen sa
82+
83+
{-
84+
85+
instance Weaken (Chain 0 a) where
86+
type Weakened (Chain 0 a) = a
87+
weaken = unChain
88+
instance Strengthen (Chain 0 a) where
89+
strengthen = Right . Chain
90+
91+
instance Weaken a => Weaken (Chain 1 a) where
92+
type Weakened (Chain 1 a) = Weakened a
93+
weaken = weaken . unChain
94+
instance Strengthen a => Strengthen (Chain 1 a) where
95+
strengthen = fmap Chain . strengthen
96+
97+
instance (Weaken a, Weaken (Weakened a)) => Weaken (Chain 2 a) where
98+
type Weakened (Chain 2 a) = Weakened (Weakened a)
99+
weaken = weaken . weaken . unChain
100+
instance (Strengthen a, Strengthen (Weakened a)) => Strengthen (Chain 2 a) where
101+
strengthen a = -- TODO how to pointfree this lol? pointfree fmap confuses me
102+
case strengthen a of
103+
Left e -> Left e
104+
Right sa -> fmap Chain (strengthen sa)
105+
-}
106+
107+
{-
108+
newtype Twice a = Twice { unTwice :: a }
109+
deriving stock Show
110+
instance (Weaken a, Weaken (Weakened a)) => Weaken (Twice a) where
111+
type Weakened (Twice a) = Weakened (Weakened a)
112+
weaken = weaken . weaken . unTwice
113+
-}

src/Strongweak/Coercibly.hs

+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
-- TODO what to name this? "coerce" has to be in it (that's the main thing).
2+
-- also trivial? zero-invariant? what about the strategy?
3+
4+
-- TODO no longer really need thanks to SWChain. but maybe keep it around idk
5+
6+
module Strongweak.Coercibly where
7+
8+
import Strongweak.Weaken
9+
import Strongweak.Strengthen
10+
import Data.Coerce
11+
import Data.Kind ( type Type )
12+
13+
{- | A @from@ that can be safely coerced between @to@.
14+
15+
You can use this to decide precisely how to weaken a newtype: whether to only
16+
strip the newtype via 'Shallow', or to strip the newtype and weaken the inner
17+
type via 'Deep'.
18+
-}
19+
newtype Coercibly (stg :: Strategy) (from :: Type) to
20+
= Coercibly { unCoercibly :: from }
21+
deriving stock Show
22+
23+
-- | How to weaken a layer type.
24+
data Strategy
25+
= Shallow -- ^ Remove the layer.
26+
| Deep -- ^ Remove the layer, and weaken the inner type.
27+
28+
-- note that without the Coercible instance, we get a confusing "couldn't match
29+
-- representation of type 'from' with that of 'to'" error message. this might
30+
-- happen in user code that tries to be parametric with 'Coercibly'
31+
32+
-- | Remove the coercible @from@ layer.
33+
instance Coercible from to => Weaken (Coercibly Shallow from to) where
34+
type Weakened (Coercibly Shallow from to) = to
35+
weaken = coerce . unCoercibly
36+
37+
-- | Remove the coercible @from@ layer and weaken the result.
38+
instance (Coercible from to, Weaken to) => Weaken (Coercibly Deep from to) where
39+
type Weakened (Coercibly Deep from to) = Weakened to
40+
weaken = weaken . coerce @from @to . unCoercibly
41+
42+
-- | An @f a@ that can be safely coerced between @a@.
43+
newtype Coercibly1 (stg :: Strategy) f (a :: Type)
44+
= Coercibly1 { unCoercibly1 :: f a }
45+
deriving stock Show
46+
47+
-- | Remove the coercible @f a@ layer.
48+
instance Coercible (f a) a => Weaken (Coercibly1 Shallow f a) where
49+
type Weakened (Coercibly1 Shallow f a) = a
50+
weaken = coerce . unCoercibly1
51+
52+
-- | Remove the coercible @f a@ layer and weaken the result.
53+
instance (Coercible (f a) a, Weaken a) => Weaken (Coercibly1 Deep f a) where
54+
type Weakened (Coercibly1 Deep f a) = Weakened a
55+
weaken = weaken . coerce @(f a) @a . unCoercibly1
56+
57+
58+
instance Coercible from to => Strengthen (Coercibly Shallow from to) where
59+
strengthen = Right . Coercibly . coerce @to @from
60+
61+
instance (Coercible from to, Strengthen to)
62+
=> Strengthen (Coercibly Deep from to) where
63+
strengthen = fmap (Coercibly . coerce @to @from) <$> strengthen
64+
65+
instance Coercible (f a) a => Strengthen (Coercibly1 Shallow f a) where
66+
strengthen = Right . Coercibly1 . coerce @a @(f a)
67+
68+
instance (Coercible (f a) a, Strengthen a)
69+
=> Strengthen (Coercibly1 Deep f a) where
70+
strengthen = fmap (Coercibly1 . coerce @a @(f a)) <$> strengthen

src/Strongweak/Example.hs

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
module Strongweak.Example where
2+
import Strongweak
3+
import Strongweak.Generic
4+
import GHC.Generics ( Generic )
5+
import Data.List.NonEmpty as NE
6+
import Data.Word
7+
8+
data A (s :: Strength) = A
9+
{ a1 :: SW s (NE.NonEmpty (SW s Word8))
10+
} deriving stock Generic
11+
deriving instance Show (A Weak)
12+
deriving instance Show (A Strong)
13+
14+
instance Weaken (A Strong) where
15+
type Weakened (A Strong) = A Weak
16+
weaken = undefined -- weakenGeneric

src/Strongweak/Generic.hs

+7-4
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,9 @@ between any /compatible/ pair of types. Compatibility is defined as follows:
3030
3131
* Both types' generic representation (the SOP tree structure) match exactly.
3232
* For each leaf pair of types, either the types are identical, or the
33-
appropriate instance exists to transform from source to target.
33+
appropriate instance exists to transform from source to target.
3434
35-
If they aren't compatible, the derivation will fail with a type error. I'm
36-
fairly certain that if it succeeds, your instance is guaranteed correct
37-
(assuming the instances it uses internally are all OK!).
35+
If they aren't compatible, the derivation will fail with a type error.
3836
3937
I don't think GHC strongly guarantees the SOP property, so if you receive
4038
surprising derivation errors, the types might have differing generic
@@ -46,6 +44,11 @@ Also, generic strengthening requires that all metadata is present for both
4644
types: for the datatype, constructors and selectors. GHC will always add this
4745
metadata for you, but manually-derived Generic instances (which are usually a
4846
bad idea) do not require it.
47+
48+
Note that the generics only handle one "layer" at a time. If you have a data
49+
type with nested 'Strongweak.Strengthen.SW' uses, these generics will fail with
50+
a type error. Write the instance manually instead. (I can't think of an easy way
51+
to handle this, but TODO all the same.)
4952
-}
5053

5154
{- | @DerivingVia@ wrapper for strongweak instances.

src/Strongweak/Strengthen.hs

+3-27
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,7 @@ module Strongweak.Strengthen
2121
) where
2222

2323
import Strongweak.Util.TypeNats ( natVal'' )
24-
import Strongweak.Weaken
25-
( Weaken(Weakened, weaken), Coercibly(..), Coercibly1(..), Strategy(..) )
24+
import Strongweak.Weaken ( Weaken(Weakened, weaken) )
2625

2726
import GHC.TypeNats ( KnownNat )
2827
import Data.Word
@@ -42,8 +41,6 @@ import Data.Bits ( FiniteBits )
4241

4342
import Data.Typeable ( Typeable, TypeRep, typeRep, Proxy(Proxy) )
4443

45-
import Data.Coerce
46-
4744
{- | Attempt to strengthen some @'Weakened' a@, asserting certain invariants.
4845
4946
We take 'Weaken' as a superclass in order to maintain strong/weak type pair
@@ -143,14 +140,6 @@ instance (VG.Vector v a, KnownNat n) => Strengthen (VGS.Vector v n a) where
143140
, "fail: wrong length (got "<>TBL.fromDec (length as)<>")" ]
144141
where n = natVal'' @n
145142

146-
-- | Add wrapper.
147-
instance Strengthen (Identity a) where
148-
strengthen = Right . Identity
149-
150-
-- | Add wrapper.
151-
instance Strengthen (Const a b) where
152-
strengthen = Right . Const
153-
154143
{- TODO controversial. seems logical, but also kinda annoying.
155144
instance (Show a, Typeable a) => Strengthen (Maybe a) where
156145
strengthen = \case [a] -> pure $ Just a
@@ -248,18 +237,5 @@ f .> g = g . f
248237
typeRep' :: forall a. Typeable a => TypeRep
249238
typeRep' = typeRep (Proxy @a)
250239

251-
instance Coercible from to => Strengthen (Coercibly Shallow from to) where
252-
strengthen = Right . Coercibly . coerce @to @from
253-
254-
-- TODO wrap errors here?
255-
instance (Coercible from to, Strengthen to)
256-
=> Strengthen (Coercibly Deep from to) where
257-
strengthen = fmap (Coercibly . coerce @to @from) <$> strengthen
258-
259-
instance Coercible (f a) a => Strengthen (Coercibly1 Shallow f a) where
260-
strengthen = Right . Coercibly1 . coerce @a @(f a)
261-
262-
-- TODO wrap errors here?
263-
instance (Coercible (f a) a, Strengthen a)
264-
=> Strengthen (Coercibly1 Deep f a) where
265-
strengthen = fmap (Coercibly1 . coerce @a @(f a)) <$> strengthen
240+
instance Strengthen (Identity a) where
241+
strengthen = Right . Identity

src/Strongweak/Strengthen/Unsafe.hs

-8
Original file line numberDiff line numberDiff line change
@@ -47,14 +47,6 @@ instance VG.Vector v a => UnsafeStrengthen (VGS.Vector v n a) where
4747
unsafeStrengthen =
4848
Data.Vector.Generic.Sized.Internal.Vector . VG.fromList
4949

50-
-- | Add wrapper.
51-
instance UnsafeStrengthen (Identity a) where
52-
unsafeStrengthen = Identity
53-
54-
-- | Add wrapper.
55-
instance UnsafeStrengthen (Const a b) where
56-
unsafeStrengthen = Const
57-
5850
{- TODO controversial. seems logical, but also kinda annoying.
5951
-- | Unsafely grab either 0 or 1 elements from a list.
6052
instance UnsafeStrengthen (Maybe a) where

src/Strongweak/Util/TypeErrors.hs

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module Strongweak.Util.TypeErrors where
2+
3+
import GHC.TypeError
4+
5+
type ErrZeroInvariantNewtype a =
6+
Text a :<>: Text " is a zero-invariant, coercible newtype"
7+
:$$: Text "These may not be used with strongweak type classes directly"
8+
:$$: Text " due to ambiguity with their handling"
9+
:$$: Text "Wrap it using Strongweak.Coercibly"

0 commit comments

Comments
 (0)