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

GHC ghc-devs at haskell.org
Mon Nov 7 15:10:12 UTC 2016


#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


More information about the ghc-tickets mailing list