I have example based on hyperloglog. I'm trying to parametrize my Container
with size, and use reflection to use this parameter in functions on containers.
import Data.Proxy
import Data.Reflection
newtype Container p = Container { runContainer :: [Int] }
deriving (Eq, Show)
instance Reifies p Integer => Monoid (Container p) where
mempty = Container $ replicate (fromIntegral (reflect (Proxy :: Proxy p))) 0
mappend (Container l) (Container r) = undefined
My lame Monoid instance defines mempty
based on reified parameter, and doing some "type-safe" mappend
. It works perfectly when I'm trying to sum containers of different size, failing with type error.
However it's still can be tricked with coerce
, and I'm looking for way to block it at compile time:
ghci> :set -XDataKinds
ghci> :m +Data.Coerce
ghci> let c3 = mempty :: Container 3
ghci> c3
ghci> Container {runContaner: [0,0,0]}
ghci> let c4 = coerce c3 :: Container 4
ghci> :t c4
ghci> c4 :: Container 4
ghci> c4
ghci> Container {runContainer: [0,0,0]}
Adding type role doesn't help
type role Container nominal
The issue is that newtype
s are coercible to their representation as long as the constructor is in scope – indeed, this is a big part of the motivation for Coercible
. And Coercible
constraints are like any other type class constraint, and are automatically searched and put together for you, only even more so. Thus, coerce c3
is finding that you have
instance Coercible (Container p) [Int]
instance Coercible [Int] (Container p')
for all p
and p'
, and happily building the composite coercion for you via
instance (Coercible a b, Coercible b c) => Coercible a c
If you don't export the Container
constructor – as you probably want to do anyway! – then it's no longer known that the newtype
is equal to its representation (you lose the first two instances above), and you get the desired error in other modules:
ContainerClient.hs:13:6:
Couldn't match type ‘4’ with ‘3’
arising from trying to show that the representations of
‘Container 3’ and
‘Container 4’ are the same
Relevant role signatures: type role Container nominal nominal
In the expression: coerce c3
In an equation for ‘c4’: c4 = coerce c3
However, you'll always be able to break your invariants in the module where you define the newtype
(via coerce
or otherwise).
As a side note, you probably don't want to use a record-style accessor here and export it; that lets users use record-update syntax to change out code from under you, so
c3 :: Container 3
c3 = mempty
c3' :: Container 3
c3' = c3{runContainer = []}
becomes valid. Make runContainer
a free-standing function instead.
We can verify that we're getting the composition of the two newtype
-representation constraints by looking at the Core (via -ddump-simpl
): within the module that defines Container
(which I've also called Container
), the output is (if we remove the export list)
c4 :: Container 4
[GblId, Str=DmdType]
c4 =
c3
`cast` (Container.NTCo:Container[0] <GHC.TypeLits.Nat>_N <3>_N
; Sym (Container.NTCo:Container[0] <GHC.TypeLits.Nat>_N <4>_N)
:: Container 3 ~R# Container 4)
It's a bit hard to read, but the important thing to see is Container.NTCo:Container[0]
: the NTCo
is a newtype
coercion between the newtype
Container p
and its representation type. Sym
turns this around, and ;
composes two constraints.
Call the final constraint γₓ
; then the whole typing derivation is
Sym :: (a ~ b) -> (b ~ a)
-- Sym is built-in
(;) :: (a ~ b) -> (b ~ c) -> (a ~ c)
-- (;) is built-in
γₙ :: k -> (p :: k) -> Container p ~ [Int]
γₙ k p = Container.NTCo:Container[0] <k>_N <p>_N
γ₃ :: Container 3 ~ [Int]
γ₃ = γₙ GHC.TypeLits.Nat 3
γ₄ :: Container 4 ~ [Int]
γ₄ = γₙ GHC.TypeLits.Nat 4
γ₄′ :: [Int] ~ Container 4
γ₄′ = Sym γ₄
γₓ :: Container 3 ~ Container 4
γₓ = γ₃ ; γ₄′
Here are the source files I used:
Container.hs:
{-# LANGUAGE FlexibleContexts, UndecidableInstances, ScopedTypeVariables,
RoleAnnotations, PolyKinds, DataKinds #-}
module Container (Container(), runContainer) where
import Data.Proxy
import Data.Reflection
import Data.Coerce
newtype Container p = Container { runContainer :: [Int] }
deriving (Eq, Show)
type role Container nominal
instance Reifies p Integer => Monoid (Container p) where
mempty = Container $ replicate (fromIntegral (reflect (Proxy :: Proxy p))) 0
mappend (Container l) (Container r) = Container $ l ++ r
c3 :: Container 3
c3 = mempty
-- Works
c4 :: Container 4
c4 = coerce c3
ContainerClient.hs:
{-# LANGUAGE DataKinds #-}
module ContainerClient where
import Container
import Data.Coerce
c3 :: Container 3
c3 = mempty
-- Doesn't work :-)
c4 :: Container 4
c4 = coerce c3
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With