1

With InstanceSignatures, I can give a signature for a method within an instance decl.

The type signature in the instance declaration must be more polymorphic than (or the same as) the one in the class declaration, instantiated with the instance type.

I can see from this answer that the type part of the sig can't be more specific; but why couldn't you add extra constraints? After all you can put constraints on the instance decl that will make the instance more specific than "the class declaration, instantiated with the instance type".

Addit: (In response to the first couple of comments.) To explain "you can put constraints on the instance decl ... instance more specific": the OVERLAPPABLE instance head here alleges it provides addNat for all types a, b, c. But it doesn't: it only provides if a is of the form SNat a', c is of the form SNat c', etc. I'm asking why I can't similarly restrict the types at the method level?

For example [adapted from Hughes 1999] (more realistically this could be a BST/Rose tree, etc):

data AscList a = AscList [a]                          -- ascending list

instance Functor AscList  where                       -- constructor class
  fmap f (AscList xs) = AscList $ sort $ fmap f xs
  -- :: Ord b => (a -> b) -> AscList a -> AscList b   -- inferred

Without an instance signature GHC complains no instance for (Ord b). With the signature GHC complains the instantiated sig from the class is more polymorphic than the one given.

This (excellent) answer explains the machinery in the instance dictionary. I can see the entry in the dictionary for the method has a type set from the number of constraints for the method (if any) in the class decl. There's no room to put an extra dictionary/parameter for the method.

That seems merely that the implementation didn't foresee a need. Is there a deeper/more theory-based reason against?

(BTW I'm not ever-so convinced by Hughes' approach: that would want both Ord a => WFT(AscList a) and Ord b => WFT(AscList b). But there's no need to require the incoming (AscList a) is ascending. Other constructor classes for AscList perhaps don't need an Ord constraint anywhere.)

Micha Wiedenmann
  • 19,979
  • 21
  • 92
  • 137
AntC
  • 2,623
  • 1
  • 13
  • 20
  • ghc's error message is quite clear here: you cannot fmap an arbitrary function and receive a sorted list. As it's currently written in your question, you don't even require `AscList`'s argument be `Ord`. – bipll Feb 10 '21 at 11:05
  • 1
    The functor class definition states that a type constructor `F` is a functor iff it provides `fmap` for all types `a` and `b`. So it's all-or-nothing, either `F` is a functor or it is not (`fmap` does not work in at least one case). This would be different if we had a functor class like `class Functor' f a b where fmap' :: (a->b)->f a->f b` where `a,b` are indices of the class, hence can be constrained in instances. In the libraries there are some variants like this but they are not frequently used in practice. – chi Feb 10 '21 at 11:56
  • Your `Functor` instance violates the law `fmap id == id`. (`id (AscList [3,2,1]) == AscList [3,2,1]`, but `fmap id (AscList [3,2,1]) == AscList [1,2,3]`.) (Though you can get away with it if you ensure that an `AscList` value can only be created with a smart constructor prevents an unsorted list from being created in the first place.) – chepner Feb 10 '21 at 15:15
  • @chepner, ah ok, so I do need `WFT (AscList a)` as proxy saying the incoming list is sorted. Then the law still holds. – AntC Feb 10 '21 at 23:17
  • Actually, I'm don't think you can preserve the other law, `fmap (f.g) == fmap f . fmap g`. If `fmap g` reorders the list, `f` is applied to different elements than if `f.g` were used directly. – chepner Feb 11 '21 at 13:29
  • @chepner, I'm pretty sure that law _does_ hold with that definition for `fmap` as given in the q. The `sort` for `fmap f` obliterates the `sort` for `fmap g`. OTOH that law doesn't hold in general: suppose the definition for `fmap` had `reverse` instead of `sort`: it'll produce either two `reverse`s or one. – AntC Feb 11 '21 at 23:46

1 Answers1

5

If you allow adding constraints to instance methods that aren't already in the class method, a polymorphic function using type classes might no longer typecheck when you specialize it.

For example, consider your AscList instance above, and the following usage of Functor:

data T f = T (f (IO ()))  -- f applied to a non-Ord

example :: Functor f => T f -> T f
example (T t) = T (fmap (\x -> x >> print 33) t)
  1. The type of example says you can instantiate f with any functor, such as f = AscList.
  2. But that makes no sense because it would try to sort an AscList (IO ()).

The only way to tell that something goes wrong when we specialize example here is to read its definition, to find whether uses of fmap are still valid, and that goes against modularity. The type class constraints in a type signature do not and should not record how their methods are used. Conversely, that means that instances do not get to add constraints to their method implementations.

Li-yao Xia
  • 31,896
  • 2
  • 33
  • 56
  • "no longer typecheck when you specialize it" is exactly what I (and Hughes, and Eisenberg with Constrained Type Functions) are trying to achieve; it's what you get with constraints on instances; it's a Good Thing to make your code type-safer. So sorry, this answer, and the comments, are telling me I can't do it because I can't do it. That's what the q says already. – AntC Feb 10 '21 at 23:12
  • I am not rejecting your example based on the mere fact that my example would lead to failure, but that there is no conceivable mechanism by which your imaginary type system can catch the failure; or if there is one, show me. The three lines I wrote presumably would type check (as they do now). If we can also write your `AscList` instance, then the only place left for a type error is at a use site of my `example` where it is specialized to `AscList` but what information can a type checker use to tell that something is wrong? – Li-yao Xia Feb 10 '21 at 23:27
  • Re: "no longer typecheck when you specialize it" I would be surprised if what I meant here is really what you or anyone would ever want to achieve. What I mean is that if a signature starts with `forall f` the domain should be well-defined. We expect to be able to put any `f` there, or if we can't, the invalidity of our choice should be explainable by additional syntactic restrictions visible in that signature or the language definition. So where is the restrictions that says I can't write `example @AscList`? – Li-yao Xia Feb 10 '21 at 23:50
  • "there is no conceivable mechanism"? There is, it's in Hughes 1999, and described in quite a bit of detail. Essentially the compiler inserts a `WFT ...` constraint on each argument to the method; the programmer gives an `instance WFT` for each of their data types, with an instance constraint `Ord a => WFT (AscList a)` in that case; the compiler enforces that constraint for any usage `fmap f (AscList ...)`. – AntC Feb 11 '21 at 01:33
  • "where is the restrictions"? Hughes is saying there are program-wide restriction that functions/methods are only valid for well-formed types. (And Eisenberg similarly for well-instantiated types.) Hughes puts the restrictions on the `data` decl. I'm asking if there's reason in principle I can't put the restrictions as constraints on method instance signatures. – AntC Feb 11 '21 at 01:41
  • Both of my comments are in the context of your proposed solution "put the restrictions as constraints on method instance signatures" so quoting Hughes's and Eisenberg's different solutions is irrelevant. Furthermore, since the very beginning I have provided a very concrete example to illustrate my questions and the problem as I see it and you have yet to explain how it would in fact work in your view or which of my assumptions in that example you disagree with. Instead you have immediately patronized my answer as "I can't do it because I can't do it" and refused to engage with it. – Li-yao Xia Feb 11 '21 at 02:39
  • I'm quoting H & E as giving use cases, which seem plausible requirements to me. (H in particular has a much longer example with monad transformers.) Your answer says "The type class constraints in a type signature do not and should not record how their methods are used." You haven't cited any reference to justify that opinion. And it's not true: instance constraints (which are not visible in "type class constraints in a type signature") do restrict how the methods can be used for some instance. It's just that with constructor classes the instance constraints can't 'reach' the arguments. – AntC Feb 11 '21 at 04:35
  • Yes, when I wrote "should not" that is my opinion, whatever. But "do not" refers to the fact that as of now the signature of `example` only has `Functor f` which says nothing about `fmap` being used at type `IO ()`. I wrote "type class constraints", I did not write "instance constraints", so again your counterclaim of "it's not true" starts with a nonexample. I am trying to make the point that "instance constraints" not appearing in the signature of functions that may use those instances is in fact a problem. You are still nitpicking the metadiscourse when I have provided a concrete example. – Li-yao Xia Feb 11 '21 at 04:50
  • I'm asking if there's a theoretical reason against; then we can move on to "how it would in fact work". So to engage with your example "The type of `example` says ..." (together with the decl for `data T`) `example` works for any `Functor f` providing there's a valid instance for `f` -- in particular for `(f (IO () ))`. Attempting to call `example` over `AscList` will be ill-typed at the call site, we can't provide a valid `t` for argument `(T t)`. – AntC Feb 11 '21 at 04:55