I've got situation where I had a product type (say T), and an instance of say C derived using anyclass deriving, which works because each member of T is an instance of C.
I changed one member of T to a type which wasn't an instance of C (lets call the type of this new member U). Note that both C and U are from external libraries, T is in my own code. This left me with a few options, that I thought of:
- Try to manually define an instance
C T, which would have required me to dig into the generic implementation and basically copy-pasting it. - Define an orphan instance
C U(as I don't controlCnorU). - Newtype
U(sayV U) and change the member to be of typeV.
But I didn't really like any of these solutions:
- I didn't really want to manually copy-paste hack up an entire manual instance of
Twhen the Generic derived instance worked quite well. - I could make an orphan instance (admittedly I think this is the best approach out of the three) but still, I hear orphans aren't great, as they can increase compile times if they're used deep in an inheritance hierarchy as every module that gets compiles that transitively depends on them has to check the interface file of the orphan module. Also there's the "orphans are bad" debate.
- Newtyping just to handle this one instance seems like an overkill. I guess then I could write an accessor that unwraps the newtype, but that doesn't solve everything, like if I want to just unwrap the type using
{}style syntax I'm still going to have to coerce the result. All this hackery just to handle one pesky instance.
So, what I was thinking is that I just need my custom instance available in this one place, not my whole program.
So I did define a newtype V = V U, and defined my instance C V. But I DIDN'T change the type T. I instead wrote the following function, using the constraints library, with some of the ideas I got from here and here:
import Data.Coerce (Coercible, coerce)
import Data.Constraint qualified as Constraint
import Data.Constraint.Unsafe qualified as Constraint
import Data.Kind (Type)
{-
Given an instance `c b`, this will give you an instance `c a` , so long as `b` is coercible to `a`.
-}
withLocalInstanceVia :: forall
(c :: Type -> Constraint)
(a :: Type)
(b :: Type)
(r :: Type).
(c b, Coercible a b)
=> (c a => r) -> r
withLocalInstanceVia = Constraint.withDict @(c a)
(Constraint.unsafeUnderive (coerce @a @b))
And then changed my previous instance like follows:
class C a where
f :: Proxy a -> SomeType
instance C T where
f = deriveGenericC
which currently wasn't compiling because there's no instance C U.
To this:
instance C T where
f = withLocalInstanceVia @C @U @V deriveGenericC
Which works fine because there IS an instance C V.
But of note is that from the constraints library I'm using the function unsafeUnderive. My question is, what exactly is unsafe about unsafeUnderive? Here's some concerns:
- Internally it uses unsafe coerce I believe, but at least unlike
unsafeCoerceConstraintit has a coercible constraint. PresumablyunsafeUnderiveis safer thanunsafeCoerceConstraintbut given the name I presume it's still unsafe. Is the unsafety just that future changes in GHC might break such a coercion, or are there some bad ways I could use this today? - Any other gotchas here? Presumably all the issues that come with orphan instances (well, aside from the orphan module issue, as the definition is local not global) but given that
Vis defined next to the instance definition and not exported, there really can't be another instanceC Vin scope anyway.