Deriving via TH (#12457)
Ryan Scott
ryan.gl.scott at gmail.com
Wed Jun 19 13:07:47 UTC 2024
Hi Sebastian,
This is exciting! I haven't taken a close look at the implementation, but
here are my initial reactions:
- The implementation of `deriving` proceeds in a somewhat unusual way:
you first typecheck the `deriving` clause and use the resulting Core types
to figure out what the instance head should be. Then, you generate the
method bindings for the instance as parsed code, then proceed to rename and
typecheck the bindings. As such, `deriving` goes from typechecking ->
renaming -> typechecking, which is somewhat odd...
- ...and derived type family instances work in an even more odd way.
Rather than generating parsed type family instance declarations (and then
renaming/typechecking them), `deriving` directly generates Core axioms for
the instances. As such, derived associated type family instances go through
a rather different code path than derived method bindings. (There are
technical reasons why this is the case, but I won't get into them here.)
This asymmetry means that the existing `deriving` machinery has to do some
odd things to make everything fit together.
- I think part of the difficulty you're encountering is that the `th`
deriving strategy is attempting to produce associated type family instances
from TH quotes (i.e., parsed code), rather than from typechecked code.
Every other deriving strategy produces its associated type family instances
from Core types, however, so they are able to generate instances without
renaming or typechecking. You don't have that luxury. As such, I agree that
you'll need to reuse other parts of the renamer and typechecker (e.g., the
`tcClsInstDecl` function) in order to turn your TH-quoted type family
instances into Core axioms.
- Is it a good idea to integrate this new deriving strategy so tightly
with the existing deriving framework? I think it's at least worth trying.
The `deriving` code path is *just* different enough from the code path
for ordinary class instances where I think you'll encounter some oddities
if you try to implement the `th` deriving strategy out of band. For
example, you'll want to be able to dump `th`-derived code that you generate
using -ddump-deriv, which currently only happens in the `deriving` code
path. I suppose you could change things so that -ddump-deriv does things in
multiple places in the code, but I worry that that may lead to an
uncomfortable amount of code duplication. (Not to mention that you'll have
to be careful to actually emulate everything that the `deriving` code path
does, because if you forget something, then that can lead to confusing bugs
down the road.)
- Of course, I won't claim that the current design of the `deriving`
code path is perfect by any means. If there are ways we could clean things
up that would make it easier to implement the `th` deriving strategy, then
we should consider doing that.
I'm not sure if I fully answered the spirit of your question, so feel free
to ask follow-up questions with specifics if I missed the mark.
Best,
Ryan
On Wed, Jun 19, 2024 at 4:52 AM Sebastian Graf <sgraf1337 at gmail.com> wrote:
> Hi Ryan and GHC devs,
>
> I'm working on-and-off on a prototype that enables use of TemplateHaskell
> as a deriving strategy:
> https://gitlab.haskell.org/ghc/ghc/-/commit/82aea77ed908fe36bed829c9c4a01ea9b30a0181
> .
> My current working example is:
>
> ```
> {-# LANGUAGE TemplateHaskell #-}
> {-# LANGUAGE TypeFamilies #-}
>
> module T12457A where
>
> import GHC.Internal.TH.Lib
> import GHC.Internal.TH.Syntax
>
> class C a where
> type Assoc a
> m :: a -> Int
> n :: a -> Int
>
> instance DeriveTH C where
> deriveTH _p head = do
> let AppT (ConT t) (VarT a) = head
> x <- newName "x"
> x2 <- newName "x"
> addTopDecls =<< [d|
> $(varP x) = 12
> $(varP x2) = 23 |]
> [d|
> instance C $(varT a) => C ($(conT t) $(varT a)) where
> type Assoc ($(conT t) $(varT a)) = Char
> m :: Eq a => a -> Int
> m _ = $(varE x) + 42
> {-# INLINE m #-}
> n :: Show a => a -> Int
> n _ = $(varE x2) + 13 |]
>
> ---
>
> {-# LANGUAGE TemplateHaskell #-}
> {-# LANGUAGE ConstraintKinds #-}
> {-# LANGUAGE DerivingStrategies #-}
>
> module T12457 where
>
> import Language.Haskell.TH
> import GHC.Internal.TH.Lib
> import T12457A
>
> newtype T a = T [a] deriving th C
> ```
>
> I just managed to implement `GHC.Tc.Deriv.Infer.inferConstraints` for this
> mechanism (still hacky and broken in many ways) and am now stuck in
> `Deriv.genFamInst`.
> I realised I would need to replicate the first half of `tcClsInstDecl` to
> implement it. Before long, I will probably also need to replicate the other
> half to check method bindings.
> That leaves me wondering: Is it a good idea to integrate this new deriving
> strategy so tightly with the existing deriving framework?
> I would rather just call `tcClsInstDecl`, do a bit of sanity checking for
> specified constraints in standalone deriving declarations and call it a day.
>
> Given that you are the architect of our current deriving code, I hope you
> are the right person to ask for input.
>
> Thanks,
> Sebastian
>
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://mail.haskell.org/pipermail/ghc-devs/attachments/20240619/d82dd388/attachment.html>
More information about the ghc-devs
mailing list