Re: Restricted Data Types
From: John Meacham <john@repetae.net> Subject: Re: Restricted Data Types however, what prevents the following from being _infered_ return Foo :: Moand m => m Foo so, we think we can specialize it to return Foo :: Set Foo however, we no longer have the constraint that Foo must be in Eq! Maybe I wasn't up-front enough about this in the paper: RDTs add a new form of constraint in contexts, wft t, meaning that type t is well-formed. While base types and type variables can be assumed to be well-formed, other types may only be used at all in the context of a suitable well-formedness constraint. That means that the Monad class is not allowed to declare return :: a -> m a because there's no guarantee that the type m a would be well-formed. The type declared for return has to become return :: wft (m a) => a -> m a i.e. when return is called, it has to be passed a dictionary proving that m a is well-formed. In the case of Set, that's an Eq a dictionary. In your example, the most general type of return Foo is (Monad m, wft (m Foo)) => m Foo, and when you instantiate m to Set, you're left with the contraint wft (Set Foo), which reduces to Eq Foo. The changes to the type system are that all typing rules have to add wft t constraints to the context, for every type t that appears in them. Fortunately most can be immediately discarded, since base types are always well-formed, and data type definitions imply constraint reduction rules that eliminate wft constraints unless the datatype is restricted. For example, wft [a] reduces to wft a, because lists place no restriction on their element type. Wft constraints on type *variables* can be discarded where the variables are generalised, *provided* the instantiation rule only permits variables to be instantiated at well-formed types. There's no need to write the type of reverse as wft a => [a] -> [a], if polymorphic type variables can only every be instantiated to well formed types. The only wft constraints that cannot be discarded are thus those where the type that must be well-formed is an application of a type constructor variable, such as wft (m a) in the type of return. That suggests that wft constraints ought not to be too common in real code, but they do crop up in monadic library code-- class Monad m where return :: wft (m a) => a -> m a (>>=) :: (wft (m a), wft (m b)) => m a -> (a -> m b) -> m b That's why, without some clever optimisation, RDTs will lead to massive extra dictionary passing (in implementations that use dictionaries). "It'll be just fine as long as you don't use monads" isn't really a good enough argument, is it?! John
John Hughes wrote:
That means that the Monad class is not allowed to declare
return :: a -> m a
because there's no guarantee that the type m a would be well-formed. The type declared for return has to become
return :: wft (m a) => a -> m a
I'm confused. It seems like the type (a -> m a) can't be permitted in any context, because it would make the type system unsound. If so, there's no reason to require the constraint (wft (m a)) to be explicit in the type signature, since you can infer its existence from the body of the type (or the fields of a datatype declaration). Okay, simplify, simplify. How about the following: For every datatype in the program, imagine that there's a class declaration with the same name. In the case of data Maybe a = ... it's class Maybe a where {} In the case of data Ord a => Map a b = ... it's class Ord a => Map a b where {} It's illegal to refer to these classes in the source code; they're only for internal bookkeeping. Now, for every type signature in the program (counting the "type signatures" of data constructors, though they have a different syntax), for every type application within the type of the form ((tcon|tvar) type+), add a corresponding constraint to the type context. E.g. singleton :: a -> Set a becomes (internally) singleton :: (Set a) => a -> Set a and fmapM :: (Functor f, Monad m) => (a -> m b) -> f a -> m (f b) becomes fmapM :: (Functor f, Monad m, m b, f a, m (f b), f b) => (a -> m b) -> f a -> m (f b) You also have to do this for the contexts of type constructors, I guess, e.g. data Foo a = Foo (a Int) becomes data (a Int) => Foo a = Foo (a Int). Now you do type inference as normal, dealing with constraints of the form (tvar type+) pretty much like any other constraint. Does that correctly handle every case? -- Ben
participants (2)
-
Ben Rudiak-Gould -
John Hughes