Re: [Haskell-cafe] decoupling type classes

On Sat, Jan 14, 2012 at 2:38 PM, Dominique Devriese
I may or may not have thought about it. Maybe you can give an example of parametric instances where there could be problems, so that I can figure out whether my system works on the example or not.
The typical example would be
instance Eq a => Eq [a] where [] == [] = True (a : as) == (b : bs) = a == b && as == bs _ == _ = False
It can handle this case, although it doesn't handle it as a parametric instance. I suspect that we don't need the concept of "parameter instances" at all. We just searches for instances recursively at the call site: 1. If "g" has an implicit parameter "f", search for values which matches the name and instantiated type in the current scope. 2. If a value is found, use it as the argument. 3. Check if the value is a function with implicit parameters, if so, search for values that matches the name and type of the implicit parameters. 4. Do this recursively until no more arguments contain implicit parameters.
This coupling you talk about is not actually there for instance arguments. Instance arguments are perfectly usable without records. There is some special support for automatically constructing record projections with instance arguments though.
Cool. So it seems to be close to what I had in mind.
I am not sure about the exact workings of your system, but I want to point out that alternative choices can be made about the workings of inferencing and resolving type-class instances such that local instances can be allowed. For example, in Agda, we do not infer instance arguments and we give an error in case of ambiguity, but because of this, we can allow local instances...
Certainly it should report error when there are ambiguities, but sometimes it should report an error even there is only one value that matches the name and type. For example, foo x = let overload bar (x:Int) = x + 1 in \() -> bar x baz = in foo (1::Int) Even if we have only one definition of "bar" in the program, we should not resolve it to the definition of "bar" inside foo. Because that "bar" is not visible at the call site "foo (1::int)". We should report an error in this case. Think of "bar" as a "typed dynamically scoped variable" helps to justify this decision.

Yin,
2012/1/14 Yin Wang
On Sat, Jan 14, 2012 at 2:38 PM, Dominique Devriese
wrote: I may or may not have thought about it. Maybe you can give an example of parametric instances where there could be problems, so that I can figure out whether my system works on the example or not.
The typical example would be
instance Eq a => Eq [a] where [] == [] = True (a : as) == (b : bs) = a == b && as == bs _ == _ = False
It can handle this case, although it doesn't handle it as a parametric instance. I suspect that we don't need the concept of "parameter instances" at all. We just searches for instances recursively at the call site:
That seems like it could work, but typically, one would like termination guarantees for this search, to avoid the type-checker getting stuck...
foo x = let overload bar (x:Int) = x + 1 in \() -> bar x
baz = in foo (1::Int)
Even if we have only one definition of "bar" in the program, we should not resolve it to the definition of "bar" inside foo. Because that "bar" is not visible at the call site "foo (1::int)". We should report an error in this case. Think of "bar" as a "typed dynamically scoped variable" helps to justify this decision.
So you're saying that any function that calls an overloaded function should always allow its own callers to provide this, even if a correct instance is in scope. Would that mean all instances have to be resolved from main? This also strikes me as strange, since I gather you would get something like "length :: Monoid Int => [a] -> Int", which would break if you happen to have a multiplicative monoid in scope at the call site? Dominique

The typical example would be
instance Eq a => Eq [a] where [] == [] = True (a : as) == (b : bs) = a == b && as == bs _ == _ = False
It can handle this case, although it doesn't handle it as a parametric instance. I suspect that we don't need the concept of "parameter instances" at all. We just searches for instances recursively at the call site:
That seems like it could work, but typically, one would like termination guarantees for this search, to avoid the type-checker getting stuck...
Good point. Currently I'm guessing that we need to keep a stack of the traced calls. If a recursive call needs an implicit parameter X which is matched by one of the functions in the stack, we back up from the stack and resolve X to the function found on stack.
foo x = let overload bar (x:Int) = x + 1 in \() -> bar x
baz = in foo (1::Int)
Even if we have only one definition of "bar" in the program, we should not resolve it to the definition of "bar" inside foo. Because that "bar" is not visible at the call site "foo (1::int)". We should report an error in this case. Think of "bar" as a "typed dynamically scoped variable" helps to justify this decision.
So you're saying that any function that calls an overloaded function should always allow its own callers to provide this, even if a correct instance is in scope. Would that mean all instances have to be resolved from main? This also strikes me as strange, since I gather you would get something like "length :: Monoid Int => [a] -> Int", which would break if you happen to have a multiplicative monoid in scope at the call site?
If you already have a correct instance in scope, then you should have no way defining another instance with the same name and type in the scope as the existing one. This is the case for Haskell. But it may be useful to allow nested definitions (using let) to shadow the existing instances in the outer scope of the overloaded call.

2012/1/16 Yin Wang
The typical example would be
instance Eq a => Eq [a] where [] == [] = True (a : as) == (b : bs) = a == b && as == bs _ == _ = False
It can handle this case, although it doesn't handle it as a parametric instance. I suspect that we don't need the concept of "parameter instances" at all. We just searches for instances recursively at the call site:
That seems like it could work, but typically, one would like termination guarantees for this search, to avoid the type-checker getting stuck...
Good point. Currently I'm guessing that we need to keep a stack of the traced calls. If a recursive call needs an implicit parameter X which is matched by one of the functions in the stack, we back up from the stack and resolve X to the function found on stack.
You may want to look at scala's approach for their implicit arguments. They use a certain to conservatively detect infinite loops during the instance search, but I don't remember the details off hand. While talking about related work, you may also want to take a look at Scala's implicit arguments, GHC implicit arguments and C++ concepts...
foo x = let overload bar (x:Int) = x + 1 in \() -> bar x
baz = in foo (1::Int)
Even if we have only one definition of "bar" in the program, we should not resolve it to the definition of "bar" inside foo. Because that "bar" is not visible at the call site "foo (1::int)". We should report an error in this case. Think of "bar" as a "typed dynamically scoped variable" helps to justify this decision.
So you're saying that any function that calls an overloaded function should always allow its own callers to provide this, even if a correct instance is in scope. Would that mean all instances have to be resolved from main? This also strikes me as strange, since I gather you would get something like "length :: Monoid Int => [a] -> Int", which would break if you happen to have a multiplicative monoid in scope at the call site?
If you already have a correct instance in scope, then you should have no way defining another instance with the same name and type in the scope as the existing one. This is the case for Haskell.
Yes, but different ones may be in scope at different places in the code, right?
But it may be useful to allow nested definitions (using let) to shadow the existing instances in the outer scope of the overloaded call.
I considered something like this for instance arguments in Agda, but it was hard to make the instance resolution deterministic when allowing such a form of prioritisation. The problem occurred if a shadower and shadowee instance had slightly different types, such that only the shadowee was actually type-valid for a certain instance argument. However, the type information which caused the shadower to become invalid only became available late in the type inference process. In such a case, it is necessary to somehow ascertain that the shadower instance is not chosen, but I did not manage to figure out how to get this right. Dominique
participants (2)
-
Dominique Devriese
-
Yin Wang