-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
First-class instance types and context attributes #712
Comments
One of the motivating examples for both ML modules and typeclasses is the "Set problem" -- let's say you have a Typeclasses solve this by ensuring uniqueness of instances. If we add named instances, we'd be breaking that property. ML modules solve this by making a set of strings using the usual operators be a different type from one using e.g. case-insensitive comparison, by virtue of abstract data types. If the programmer forgets to make the representation of the type abstract, these two set types will unify, which is typically a bug. One way you might do this in Coq, which is also the way I'm noting here, is that you'd parameterize the type itself with an implicit argument whose type is an instance type. When you write something like This does require the good design sense to use the function as a type argument rather than as a field, but designing a generic type is less common than using one, so from an ergonomics perspective, this seems strictly better than not tracking this at the type level. The real problem is that we need some way of getting a superclass instance out of a child one. For example, if (Ed Kmett notes that this issue crops up with ML modules as well, and that avoiding it is an advantage of typeclasses.) I think this is enough that my position on named instances is "don't bother having them" at this point... First-class instance types are probably fine, as long as we maintain the property that there's exactly one inhabitant of each instance type. |
Writing up my thoughts on what I was proposing a bit more concretely. I realize this issue has become the size of a blog post, but I hope it is easy to follow anyway.
Motivation
Consider a library of polymorphic concrete syntax utility nonterminals, as proposed in #71. To use these in e.g. ableC, one would like to use the
ast
andunparse
attributes. One approach is to use occurs-on constraints on the production:However there are a few issues with this:
item
nonterminal to have theunparse
attribute. Some host languages might not care about unparsing concrete syntax and omit this. Having a second version ofZeroOrMore
withoutunparse
would get confusing quickly.ZeroOrMore
to make use of anything besidesunparse
andast
onitem
. Maybe we want to use a different ast attribute that depends on an inherited attribute.a
type parameter should be inferrable fromitem
, i.e. one would like to writeZeroOrMore<Stmt_c>
, notZeroOrMore<Stmt_c Stmt>
. I don't think this one is solvable without adding functional dependencies and majorly overhauling Silver's type inference. But we could add an extension that would let us just writeStmt_c*
as a type expression forwarding to the above, that just does the lookup of theast
occurence onStmt_c
.Another motivation: equality attributes on polymorphic nonterminals require an
Eq
constraint on the polymorphic child. This currently means the production needs to have anEq
constraint. But we might not need theEq
constraint for other attributes on the nonterminal, or sometimes want to add an equality attribute in an extension.First-class instance types
The solution I'm proposing for this involves first-class instances as types. Essentially, everything that we currently write as a type constraint would become a type expression that can be written anywhere that we use types. The value of an instance type would be the instance dictionary or whatever value is currently being passed automatically when the instance is resolved. For example
Eq Integer
would be a type whose values are the instance object containing theeq
implementation to use for integers, andattribute unparse {} occurs on Stmt
would be a type, whose value is the index of the attribute occurrence forunparse
onStmt
.Note that there is an unfortunate parse ambiguity with regular type class constraints, if they are to become type expressions. Consider parsing
Maybe<Eq String>
: we would like to parse this asMaybe<(Eq String)>
, notMaybe<(Eq) (String)>
. I think the fix for this is to change the type class syntax to use angle brackets (e.g.Eq<String>
instead ofEq String
), which is another annoying refactor but shouldn't be nearly as bad as removing autocopy.There would be a new expression for a hole of some instance type, whose value should be computed by instance resolution. I'm not too sure about the syntax for this (
_
is taken by partial application, so maybehole
or just?
). At inference time?
would have typea
, which would must be resolved to some instance type. E.g.let eqInt :: Eq Integer = ? in ... end;
We would also need syntax for explicitly using some instance value in an expression. This is analogous to explicitly passing an implicit argument in Idris, for example. However as Silver functions aren't curried and we have non-function expressions (e.g. attribute access) that perform instance resolution, this should probably be a new expression
using <instance> in <body> end
where<instance>
is an expression that must have some instance type, and within<body>
the context corresponding to the type of<instance>
would be available. For example, this would let one writeNote to self: anywhere that something resolves using a context introduced in this way, we need to emit a flow dependency on the instance expression.
Fun aside: named instances
Another cool feature that would be easy to add (and helpful for testing) would be named instances. I think @remexre was proposing something like this at one point. This would provide a neat solution to cases where there is more than one valid instance for a type. We could write things like
Here writing a named instance would define a value of that instance type, rather than adding a normal instance definition. The
using
declarations on theMonoid
instances are needed to specify how to resolve theSemigroup
superclass ofMonoid
. We could write similar instances for conjunction/disjunction, too.In theory this might also point towards supporting multiple occurences of the same attribute on the same nonterminal, potentially with different flow types. But that would create some issues with the flow analysis, and probably isn't worthwhile.
Context attributes
The intuition behind context attributes is fairly simple: synthesized attributes are "sorta" functions, inherited attributes are "sorta" function parameters, type constraints are essentially just implicit extra function paramters passing instance dictionary objects, so context attributes are just extra inherited attributes passing instance objects. Any instance attributes occuring on a production would have their types added to the type class instance/occurs-on environment for the production body. For example we can now write:
The infered flow type of
unparse
onZeroOrMore
would now be{unparseInst}
, and the flow type ofast
onZeroOrMore
would be{astInst}
. If one of these equations was omitted in thecompoundStmt_c
production, a flow error would result. Ifunparse
didn't occur onStmt
, a type error would be raised on the?
in theunparseInst
equation ofcompoundStmt_c
. This isn't a perfect example since both attributes have empty flow types, but if we instead hadcontext attribute astInst<item a>::attribute ast<a> {env} occurs on item;
, then the inferred flow type forast
would be{astInst, env}
.I also considered that perhaps these inherited equations for context attributes should be added implicitly to every decoration site whenever the context can be resolved. This behavior would be more in line with type class constraints, which are always solved implicitly; doing so would slightly improve the ergonomics of the above, since writing the inherited equations on
l
is a bit of an annoyance. However, an unsolvable context (e.g.unparse
not occuring onStmt
) would result in an unhelpful missing equation error at the use site (e.g. the access ofunparse
onl
) instead of a nicer type error on the inherited equation. There would be a number of implementation issues as well - a context attribute occurence would need to generate implicit aspect equations for all decoration sites of the nonterminal, including ones in other non-imported grammars. There would be issues with orphaned equations similar toautocopy
as well, but this wouldn't be possible to resolve at runtime.We could make the
propagate
behavior of context attributes just generate hole equations on all children with the attribute, so we could just writepropagate unparseInst, astInst;
in every production that makes use of these combinator nonterminals. That isn't so bad, I guess.The text was updated successfully, but these errors were encountered: