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
Add a Prim.Row.Closed
compiler solved class
#3800
Comments
Seems reasonable to me 👍 |
This seems a bit dodgy to me. Doesn’t it depend on how far we are through type checking; unknowns could become concrete types as a result of later inference? For example, would something like this be a problem?
|
Can you define "be a problem"? It isn't clear to me what problem you are illustrating. An alternative may be just asserting it's closed and unifying it's tail with |
I agree with Harry, I find it a little concerning that the solution to the constraint would change depending on when/where it's solved. To me that breaks something like a principle of substitution: I mean, instance chains have to guard for apartness before they can reject a match, right? I see this as the same kind of thing. Just because we have |
I guess I’m imagining that this might cause implementation details of the compiler (i.e. the order in which things are checked) to affect resolving this class in potentially surprising ways. In that example I’m not sure whether I should expect the first conjure to resolve to a row with just |
OK, lets take a step back then. Basically I want it to accept the most specific thing possible, rather than the most general thing possible, which to me means early solving and unification, effectively short-cutting the compilers desire to make record consumption as polymorphic as possible. You are saying "It could be more later", and I don't really want it to be more later, I want it to commit to something as early as possible based on the argument provided. Maybe the class proposed is too general and there's something else that can be done. |
I understand your use case, and I'll try my utmost to respect it. But I am still concerned about how it fits into the larger scheme – that is, I'm not convinced that constraints are the right way to solve this problem. Would it ever make sense to use/have a constraintInContext ::
forall r. Closed ( a :: String | r ) => ... =>
({ a :: String | r } -> z) -> z
constraintInContext f =
let
r = conjure f
in ... I suppose the difference would be that Why not approach your problem in other ways? I recognize it is more typing, but something like specify :: forall a b. (a -> Tuple a b) -> a -> b
specify f a = snd (f a)
toBeJust :: forall a b. a -> b -> Tuple a b
toBeJust = Tuple
-- test :: forall r. Semiring r => { a :: r, b :: r } -> r
test = specify \{ a, b } -> toBeJust { a, b } $ a + b Output:
test ::
forall t5.
Semiring t5 => { a :: t5
, b :: t5
}
-> t5
|
I am aware there are all sorts of ways to add annotations in cute ways. I've come up with several, but my use case is that the arguments demanded by the function are meant to be the annotations so the additional annotations are just faff that add no extra information. I'm fine with accepting that it makes no sense, but I would at least like to know with certainty why other than "it looks a little funny maybe" 😄. I fully recognize that in the truly general case, that may be true, but is it really a problem or just potentially unintuitive if used in a certain way? Like, does it break coherence or something? It's something that does come up with some frequency in the Slack chat. Maybe another alternative is a class that eagerly returns the tail, and the user can do what it wants with it, which may be to constrain it or unify it with |
Maybe it just doesn't make sense as a general constraint, as you said? Is there something that would make it make sense? |
Another avenue might be, is there a way to have a |
I think maybe the least gross thing with what we have would be indexed applicative-do (with type applications). example = conjure Deref.ado
foo <- deref @"foo"
bar <- deref @"bar"
in ... |
Okay, I think I'm now fairly convinced that this can work. This is my current understanding of what the best solution would be:
My arguments for this:
My only remaining concern is that, like Harry said, implementation details may be leaked, and we might solve too early. But I do think it is pretty clear when constraint solving happens, so hopefully that means it is okay to produce mild effects at the same time. We do solve constraints after unifying the function on the basis of its arguments, right? (If not, we would end up with it always wanting to unify to the empty row, and then realizing that the argument asked for labels If this actually works for the use case of |
For reference, here is an indexed applicative approach. Edit: Another that only lets you apply field names, and loses a parameter. |
Deferring the constraint doesn't make sense to me, since every row is "eventually closed". If it's not closed now, it will be closed when the function is instantiated, or that function's caller is instantiated, and so on ... by the time you reach Regardless of whether this makes sense from a type system point of view (I feel like it doesn't, but I don't have a proof of unsoundness or anything), I think it would be a very unintuitive addition that wouldn't fit with the rest of the type system, hard to understand or teach. There are also workarounds, like creating a newtype over a closed row. That's what newtypes are for, after all, and if the solution to this case is a special thing in the compiler, it opens the door to all sorts of other changes solve slightly annoying things with classes for which the solution is "use a newtype". |
A newtype doesn't help for this, because you still need a
Can you qualify this in some way? (I'm being genuine). You've just stated it's unintuitive and hard to teach without saying what about it is unintuitive or hard to teach. Surely no one has tried to teach this! I fully recognize that what I've proposed may be very unintuitive, but maybe there is something that isn't? Something like this does come up with some frequency among the community, and I even find myself wanting it. I would at least like to be able to explain it in a clear, simple way that's not "it might be a little funny". Right now it essentially goes "Why is it funny?", "Because that's how it is." and I recognize that I don't actually know why it (or something like it) is fundamentally a bad idea.
I personally find this somewhat disingenuous. We have no maintenance guidelines as to what does or does not make a good compiler solution. |
Like I said, I don't have a proof that something is wrong here. I don't have time to try to produce a proof, or I would try. This just jumped out at me as I was browsing the issue list as something that made me uncomfortable enough to comment. I don't know why you think I'm being "disingenuous", but I think it's best at this point if I just bow out of the discussion here, and maybe in general. |
I wasn't asking for a proof, I just want to know what your thoughts are on it in more detail in a way that I can learn from and distill to others. "Unintuitive and hard to teach" followed by "special casing in the compiler" is a really strong criticism, but without elaborating why it doesn't help me or the discussion (especially when it garners thumbs ups) except to shut it down. |
I'm not trying to "shut it down", I'm just trying to say that I think this is a bad idea in as succinct a way as I can. I appreciate that my role in the creation of the compiler probably means my comments tend to carry a bit of extra weight, which is a big part of why I don't really comment any more, but maybe that makes things worse, I don't know. Anyway, I'd still like a way to be able to make a comment on things that I find troubling. I think @hdgarrood's example is a good one for illustrating why this isn't intuitive. I don't understand how you could implement this without special-casing something somewhere. It just doesn't seem to interact well with the way solving works by unification/substitution already. That's what I mean by hard to understand and teach - users gain an intuition for how that works, and this seems like it will behave differently. Complex type classes already seem to act magically if you don't understand how they're solved, but you can always follow the chain of dependencies to figure it out, and you only need to understand a small core set of concepts (fundeps, instance chains, etc.) to do so. I don't think this maintains that property, but as I say, I don't have a better example than Harry's, or a proof, so take that for what it is, it's just an intuition. |
Would adding syntax for a record binder which only has the fields listed in the binder and no others solve the need? Eg, if I could write, I dunno,
which would then have the inferred type |
(Because if so I’d be more comfortable with that.) |
Ok, here's a concrete complaint. Given: conjure \{ a } -> ... Why is I prefer @hdgarrood's closed record binders idea, but personally, I'd be interested to see other use cases before adding either of these new features. |
Yes, syntax is an option as well. My thinking was:
Maybe it's that for this to work the way that's intended, this would have to be solved as part of typechecking and not constraint solving. Is that the special case? |
Wrt to @hdgarrood's example: eg =
[ conjure \r@{ a } -> r
, conjure \r@{ b } -> r
] I would want this to just say it can't unify |
What would it mean for it to be part of typechecking? |
I think it would mean that it would need to be solved eagerly as part of the application judgement, so that when it sees |
I've found the occasional want to do
RowToList
magic on a function that takes a record as an argument. The problem is these are inferred polymorphically, or at least the record tail is left unknown.Something like this doesn't compile because the type of
r
in this case will involve an unknown for the tail andRowToList
requires an empty row terminator. Making this compile requires a type signature on the lambda closing the row. However, if we can conjure a record with only the required fields, this is totally safe.Could we have something like
Where the compiler yields a closed row with only the known components of the row? This would allow us to type the example as
Forcing the unknown tail to unify with
()
.The text was updated successfully, but these errors were encountered: