[GHC] #12814: Should GND infer an instance context when deriving method-free classes?

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler | Version: 8.0.1 (Type checker) | Keywords: | Operating System: Unknown/Multiple Architecture: | Type of failure: None/Unknown Unknown/Multiple | Test Case: | Blocked By: Blocking: | Related Tickets: #11369, #12810 Differential Rev(s): | Wiki Page: -------------------------------------+------------------------------------- This is a design question that emerged from code that I originally discovered [https://ghc.haskell.org/trac/ghc/ticket/11369#comment:17 here] and [https://ghc.haskell.org/trac/ghc/ticket/12810#comment:3 here]. To recap, it's now possible to have code like this: {{{#!hs {-# LANGUAGE GeneralizedNewtypeDeriving, TypeFamilies #-} class C a where type T a newtype Identity a = Identity a deriving C }}} Compiling this (with `-Wredundant-constraints`) generates this code: {{{#!hs instance C a => C (Identity a) where type T (Identity a) = T a }}} But now GHC will complain: {{{ • Redundant constraint: C a • In the instance declaration for ‘C (Identity a)’ }}} This warning makes sense from the perspective that the `C a` constraint isn't ever used by the associated type family instance. So the question arises: should GND avoid generating an instance context for the representation type in the event it's deriving a class with no methods? Fortunately, patching GHC to do this is trivial: {{{#!diff diff --git a/compiler/typecheck/TcDeriv.hs b/compiler/typecheck/TcDeriv.hs index 4722f16..df2d3d5 100644 --- a/compiler/typecheck/TcDeriv.hs +++ b/compiler/typecheck/TcDeriv.hs @@ -1272,7 +1272,8 @@ mkNewTypeEqn dflags overlap_mode tvs [ let (Pair t1 t2) = mkCoerceClassMethEqn cls dfun_tvs inst_tys rep_inst_ty m in mkPredOrigin (DerivOriginCoerce meth t1 t2) TypeLevel (mkReprPrimEqPred t1 t2) - | meth <- classMethods cls ] + | meth <- meths ] + meths = classMethods cls -- If there are no tyvars, there's no need -- to abstract over the dictionaries we need @@ -1281,7 +1282,11 @@ mkNewTypeEqn dflags overlap_mode tvs -- instance C T -- rather than -- instance C Int => C T - all_preds = rep_pred_o : coercible_constraints ++ sc_theta -- NB: rep_pred comes + all_preds = if null meths then [] else [rep_pred_o] + -- See Note [GND and method-free classes] + ++ coercible_constraints + ++ sc_theta + -- NB: rep_pred_o comes first ------------------------------------------------------------------- -- Figuring out whether we can only do this newtype-deriving thing }}} After implementing this patch, I ran the testsuite, and there were some surprising results. One thing that shocked me was that the program reported in #6088, which had previously failed due to a patch resulting from #8984, was now passing! {{{#!hs {-# LANGUAGE TypeFamilies #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE EmptyDataDecls #-} module T6088 where class C a newtype A n = A Int type family Pos n data True instance (Pos n ~ True) => C (A n) newtype B n = B (A n) deriving (C) }}} That is because previously, GHC was trying to generate an instance like this: {{{#!hs instance (Pos n ~ True) => C (B n) }}} And this was rejected, since we don't infer exotic equality constraints when deriving. But with this patch, it now generates: {{{#!hs instance {- Empty context => -} C (B n) }}} Which is certainly valid. But is it what a user would expect? I'm not so sure. As hvr notes in #11369, sometimes empty classes are used to enforce invariants, like in the following case: {{{#!hs class Throws e readFoo :: Throws IOError => FilePath -> IO Foo readFoo fn = {- ... -} }}} What if you wanted to have a `Throws` instance for a newtype? You'd probably want something like this: {{{#!hs newtype Id a = MkId a instance Throws a => Throws (Id a) }}} Which feels like something GND should be able to take care of with ease. But to your surprise, you try this: {{{#!hs newtype Id a = MkId a deriving Throws }}} And now GHC generates not the instance above, but rather just: {{{#!hs instance Throws (Identity a) }}} So it's possible that we would lose some of the expressiveness of GND by implementing this change. Is that acceptable? I'm not sure, so I though I'd solicit feedback here. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Changes (by RyanGlScott): * cc: goldfire, simonpj (added) -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:1 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by goldfire): Frankly, I was surprised that GND assumes any class constraint at all. Why should it be different than other forms of `deriving`? Just infer the class constraint based on the constraints that arise when type-checking the generated definition. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:2 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

Frankly, I was surprised that GND assumes any class constraint at all. Why should it be different than other forms of `deriving`? Just infer the class constraint based on the constraints that arise when type-checking
#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by RyanGlScott): Replying to [comment:2 goldfire]: the generated definition. I'm not sure how the thing you're imagining is different from what actually happens. GND must infer a constraint (applied to the representation type for the newtype) in order to typecheck any calls to `coerce` that GND generates. This is quite similar to stock deriving. For example, in: {{{#!hs data Foo a b = Foo b deriving Show }}} You must infer `Show b` in order to typecheck the generated `showsPrec` function. The only difference in this scenario is that there are no stock derivable classes without methods, but with GND, such a thing is possible. So it's quite a different beast. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:3 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by simonpj): Replying to [comment:2 goldfire]:
Just infer the class constraint based on the constraints that arise when type-checking the generated definition.
I agree with this; and it's precisely what Ryan's patch does. `mkNewTypeEqn` generates the constraints that are necessary to satisfy the instance decl; they are then simplified, perhaps to nothing at all. We are going to generate {{{ instance ?? => C (N a) where ...type instances... op = coerce (op :: rep-ty) }}} So for each method (if any) we need * `C rep_ty` so that we have the underlying `op` * `op_ty[rep_ty] ~R op_ty[N a]` to for the coercion of the `op` If there are no methods we don't need any of those constraints. Ryan's notes above show that the result may perhaps not be what you want, but if not, just use a standalone deriving and specify the instance context. So I vote for this patch! -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:4 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by goldfire): I suppose so. But it would also seem possible to skip figuring out, a priori, what constraints should be there. Can't we just type-check the generated code and then posit whatever constraints are necessary for that code to be accepted? That's what we do for `stock` instances, no? -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:5 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by RyanGlScott): Replying to [comment:5 goldfire]:
Can't we just type-check the generated code and then posit whatever constraints are necessary for that code to be accepted? That's what we do for `stock` instances, no?
Currently, no. The code for this is [http://git.haskell.org/ghc.git/blob/2e8463b232054b788b73e6551947a9434aa76009... inferConstraints] in `TcDerivInfer`. For stock deriving, it walks over each field of each constructor for a datatype, puts the class constraint on the field's type, and then simplifies. So `data Foo a b = Foo a Int deriving Show` becomes `(Show a, Show Int)` becomes `Show a`. As for why this is done this way, I'm not sure, but I do know that GHC figures out the instance context //before// ever generating the class method implementations (possibly for typechecking purposes). -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:6 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by goldfire): Yes, I suppose I knew that, when I think about it. But why don't we do it the other way? (This is an honest question -- not just trying to be a gadfly.) It seems easier and more robust. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:7 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by simonpj): The rationale is this. * For 'deriving' clauses we need to figure out what the context of the derived instance declaration should be. * What we ''actually'' do is this: we gather together all the constraints that ''will'' be required, and simplify them to make a minimal instance context. * We could instead (a) generate the method bindings etc, (b) typecheck them, (c) figuring out the constraints that are needed to do so, and minimise them (d) turn all that into an instance decl (e) typecheck the instance decl with all its methods. But typechecking instance decls is already fairly complicated and (a-c) sounds tricky to me. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:8 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by goldfire): I'm happy with Ryan's patch, too. But I'm still think my new approach would be simpler than the existing one. (a) is already done. That's !TcGenDeriv. (b) is already done: `tcInstDcls` (or similar) (c) will be the set of unsolved wanted constraints left over after solving. These will have to be checked for exoticness, but that's it! (d) is simply taking the result of (c) and sticking it in the instance decl. There is a matter that (c) outputs `Type` and (d) will want `HsContext` -- but I think we already have a way of squirreling a `Type` inside of an `HsType`, so this shouldn't be bad. (e) would be necessary, and it would be repeated work. So this new approach is probably slower than the existing one. In exchange, we get to delete the first several hundred lines of !TcDerivInfer (`inferConstraints`). In any case, if it's not broken, perhaps we shouldn't fix it. I just find that special-casing the 0-method case to be a bit odd. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:9 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by rwbarton): I am sure you understand this better than I do, but what happens in a case involving non-regular recursion (e.g. deriving `Eq` for `data X a = A a | B (X (a, a))`)? I argued for the same approach in the setting of `DeriveAnyClass`, and was disappointed that instead a heuristic was adopted that has no reason to be correct in general, so I think something is a bit "broken" here, in the sense of your last paragraph. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:10 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Comment (by simonpj): It's really not simple... one instance may need other instances, so we need to solved simultaneous equations; see `Note [Simplifying the instance context]` in `TcDerivInfer`. Also we aren't really special-casing the 0-method case. For each method we need 1. `C rep-ty (same for each method) 2. A coercible constraint (different for each method) Instead of generating N copies of (1) we generate just 1. But if N=0 we can generate zero of them. -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:11 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes?
-------------------------------------+-------------------------------------
Reporter: RyanGlScott | Owner:
Type: bug | Status: new
Priority: normal | Milestone: 8.2.1
Component: Compiler (Type | Version: 8.0.1
checker) |
Resolution: | Keywords:
Operating System: Unknown/Multiple | Architecture:
| Unknown/Multiple
Type of failure: None/Unknown | Test Case:
Blocked By: | Blocking:
Related Tickets: #11369, #12810 | Differential Rev(s):
Wiki Page: |
-------------------------------------+-------------------------------------
Comment (by Simon Peyton Jones

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: RyanGlScott Type: bug | Status: new Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Wiki Page: | -------------------------------------+------------------------------------- Changes (by simonpj): * owner: => RyanGlScott Comment: Ryan, I suggest you pull the trigger on this -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:13 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: RyanGlScott Type: bug | Status: patch Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Phab:D2692 Wiki Page: | -------------------------------------+------------------------------------- Changes (by RyanGlScott): * status: new => patch * differential: => Phab:D2692 -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:14 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes?
-------------------------------------+-------------------------------------
Reporter: RyanGlScott | Owner: RyanGlScott
Type: bug | Status: patch
Priority: normal | Milestone: 8.2.1
Component: Compiler (Type | Version: 8.0.1
checker) |
Resolution: | Keywords:
Operating System: Unknown/Multiple | Architecture:
| Unknown/Multiple
Type of failure: None/Unknown | Test Case:
Blocked By: | Blocking:
Related Tickets: #11369, #12810 | Differential Rev(s): Phab:D2692
Wiki Page: |
-------------------------------------+-------------------------------------
Comment (by Ben Gamari

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: RyanGlScott Type: bug | Status: closed Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: fixed | Keywords: Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Phab:D2692 Wiki Page: | -------------------------------------+------------------------------------- Changes (by bgamari): * status: patch => closed * resolution: => fixed -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:16 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler

#12814: Should GND infer an instance context when deriving method-free classes? -------------------------------------+------------------------------------- Reporter: RyanGlScott | Owner: RyanGlScott Type: bug | Status: closed Priority: normal | Milestone: 8.2.1 Component: Compiler (Type | Version: 8.0.1 checker) | Resolution: fixed | Keywords: deriving Operating System: Unknown/Multiple | Architecture: | Unknown/Multiple Type of failure: None/Unknown | Test Case: Blocked By: | Blocking: Related Tickets: #11369, #12810 | Differential Rev(s): Phab:D2692 Wiki Page: | -------------------------------------+------------------------------------- Changes (by RyanGlScott): * keywords: => deriving -- Ticket URL: http://ghc.haskell.org/trac/ghc/ticket/12814#comment:17 GHC http://www.haskell.org/ghc/ The Glasgow Haskell Compiler
participants (1)
-
GHC