Ticket #1965 (new feature request)

Opened 4 years ago

Last modified 5 months ago

Allow unconstrained existential contexts in newtypes

Reported by: guest Owned by:
Priority: normal Milestone: 7.6.1
Component: Compiler Version: 6.8.1
Keywords: Cc: pumpkingod@…, ireney.knapp@…, mokus@…
Operating System: Unknown/Multiple Architecture: Unknown/Multiple
Type of failure: None/Unknown Difficulty: Unknown
Test Case: Blocked By:
Blocking: Related Tickets:

Description

Declarations like

newtype Bar = forall a. Bar (Foo a)

ought to be allowed so long as no typeclass constraints are added. Right now, this requires data rather than newtype.

Change History

Changed 4 years ago by simonpj

Why is this useful?

In GHC it's disallowed because an existential is modeled as a data constructor with a field that captures the type in the same way that it captures the value fields. But since the representation of a newtype is supposed to be the same as the representation of the (single) field, you can't do that.

So, it'd be quite difficult to make GHC do this (i.e. it'd affect GHC's typed intermediate language); and I can't see any useful applications.

Simon

Changed 4 years ago by igloo

  • status changed from new to closed
  • resolution set to wontfix

Please reopen this ticket if you have a useful application for it.

Changed 4 years ago by simonmar

  • architecture changed from Unknown to Unknown/Multiple

Changed 4 years ago by simonmar

  • os changed from Unknown to Unknown/Multiple

Changed 5 months ago by pumpkin

  • cc pumpkingod@… added
  • failure set to None/Unknown
  • status changed from closed to new
  • resolution wontfix deleted

This would actually be a useful feature with GADTs, since matching on one can tell us what the existential field was, even without storing a context for it in our newtype.

It's definitely not essential to anything I'm doing, but perhaps the recent changes to GHC since the last time people looked at this ticket make this feature easier to implement?

Changed 5 months ago by Irene

  • cc ireney.knapp@… added

Okay, so this ticket-revival stemmed from a question I asked on IRC, so here's what I wanted to use this for.

class (Monad (m context)) => MonadContext m context where
  getContext
    :: m context context
  withContext
    :: MonadContext m context'
    => context'
    -> m context' a
    -> m context a


data FallibleContextualOperation failure context a =
  FallibleContextualOperation {
      fallibleContextualOperationAction :: context -> IO (Either failure a)
    }


instance Monad (FallibleContextualOperation failure context) where
  return a = FallibleContextualOperation {
                 fallibleContextualOperationAction = \_ -> return $ Right a
               }
  x >>= f =
    FallibleContextualOperation {
        fallibleContextualOperationAction = \context -> do
          v <- fallibleContextualOperationAction x context
          case v of
            failure@(Left _) -> return failure
            Right y -> fallibleContextualOperationAction (f y) context
      }


instance MonadContext (FallibleContextualOperation failure) context where
  getContext =
    FallibleContextualOperation {
        fallibleContextualOperationAction =
          \context -> return $ Right context
      }
  withContext context' x =
    FallibleContextualOperation {
        fallibleContextualOperationAction =
          \context -> fallibleContextualOperationAction x context'
      }

I can provide more details of what this is motivated by upon request, but basically, I want to have my "master implementation" of this class based on FallibleContextualOperation?, and then I want to make two types that get that behavior for free:

newtype Serialization context a =
  forall backend
  . Serialization {
        serializationOperation :: FallibleContextualOperation
                                    (SerializationFailure backend)
                                    context
                                    a
      }
deriving instance Monad (Serialization context)
deriving instance MonadContext Serialization context


newtype Deserialization context a =
  forall backend
  . Deserialization {
        deserializationOperation :: FallibleContextualOperation
                                    (DeserializationFailure backend)
                                    context
                                    a
      }
deriving instance Monad (Deserialization context)
deriving instance MonadContext Deserialization context

The point is so that I can then do:

class Serializable context a where
  serialize :: Serialization context a
  deserialize :: Deserialization context a

... and write many instances of this, elsewhere in my program, which don't need to know or care about the fact that there are two backend types (ByteStrings? and open files), or any of this other nasty plumbing detail.

I don't know if this is sufficient motivation to justify the work the ticket is requesting, since I don't know just how much work it is. But it's presented here for everyone's edification. :)

Changed 5 months ago by mokus

  • cc mokus@… added

I also have run into cases where I'd like to make a "trivial" GADT into a newtype. In particular, when programming in a "dependent"-like style using GADTs to encode the tags of dependent sums, it's often nice to have a wrapper that does nothing but alter the type index - a very general example would be "data Map f t a where Map :: t a -> Map f t (f a)" - which allows transforming the last type parameter by an arbitrary type-level function. Most of the time this usage could be avoided by including 'f' in the dependent sum type itself, but that complicates other things and even then I've found you occasionally want to modify the types of tags.

Another situation where I've wanted something like this to forget a phantom type - and I've seen the same pattern in other people's code I've read on hackage.

There is a different possible resolution to this request, which would probably be more work but not break portability - guarantee that any "data" declaration satisfying certain criteria will be compiled to a newtype-like representation. Here's my stab at that set of criteria:

1. Only one constructor

2. Only one field with nonzero width in that constructor (counting constraints as fields)

3. That field is marked strict

It seems like those requirements should be sufficient to justify special-case handling to compile them to something effectively the same as a newtype. Or if some mechanism already causes this to effectively by the case, then I'd be happy with that being documented and test-cases added to ensure it continues to be a stated goal to cover situations like these.

Changed 5 months ago by igloo

  • milestone set to 7.6.1

Changed 5 months ago by simonpj

Anything involving existentials is going to be hard to implement using newtype directly. But as 'mokus' says, it might be possible to make a guarantee, right in the code generator, that certain sorts of data types do not allocate a box. The conditions are, I think, very nearly as 'mokus' says:

  1. Only one constructor
  2. Only one field with nonzero width in that constructor (counting constraints as fields)
  3. That field is marked strict
  4. That field has a boxed (or polymorphic) type

I think this'd be do-able. The question is how important it is in practice; it's one more thing to maintain.

Simon

Note: See TracTickets for help on using tickets.