Open apblack opened 6 years ago
According to the spec, graceObject contains the methods isMe, !=, asString, asDebugString, and ::. Type Object contains all of these except isMe (because it is public). Because Object has != and ::, it is distinct from Object (and is a supertype of it). This seems reasonable to me, as any expression should be usable as a statement (which is expected to have type Done).
I thought that we had changed that. @kjx and I talked about it while I was in NZ, and the reduced version of Object
went into the SmallGrace library, but maybe we didn’t make the corresponding change to the Spec.
!=(_)
was in graceObject
only because traits didn’t work; once they started working, it was more logical to remove !=(_)
and put all of ==(_)
, !=(_)
and hash
into an identityEquality
trait, which defines ==(_)
using isMe(_)
. The operation ::(_)
should be there too, for reasons already explained. We also have an equality
trait, which requires ==(_)
and hash
, and provides !=(_)
and ::(_)
. That just leaves the string conversion methods.
As I wrote in email on 19 August ("Re: odd error"), I can see three resolutions to this issue.
Done
the empty interface (@kjx wanted this, I think); Done
and Object
the same, but keep the two names for documentation purposes (the current situation in minigrace); orObject
from Done
by adding a marker method to Object
(e.g., give graceObject
a method iAmAnObject -> Boolean
, and make Object
demand that method too). In debugging, I find it very helpful that Done has the asString method. Hence I would be unhappy to lose that. I don’t see the point of adding iAmAnObject — and don’t want to have to explain it to novices. As a result, I guess I’m in favor of (2).
Kim
On Oct 5, 2018, at 10:49 AM, Andrew Black notifications@github.com wrote:
As I wrote in email on 19 August ("Re: odd error"), I can see three resolutions to this issue.
Make Done equivalent to None, ie.e, Done is the empty interface (@kjx https://github.com/kjx wanted this, I think); leave Done and Object the same, but keep the two names for documentation purposes (the current situation in minigrace); or distinguish Object from Done by adding a marker method to Object (e.g., give graceObject a method iAmAnObject -> Boolean, and make Object demand that method too). — You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/gracelang/language/issues/161#issuecomment-427446557, or mute the thread https://github.com/notifications/unsubscribe-auth/ABuh-vxEFq3i_k3DGcYf5Xi8T1MsNizgks5uh5uOgaJpZM4SXtdF.
On 6/10/2018, at 8:58AM, Kim Bruce notifications@github.com wrote:
and don’t want to have to explain it to novices
well once again this is a point where the Right Thing isn't helpful to novices. doesn't mean we shouldn't do what's helpful, even if it's the Wrong Thing.
• Make Done the empty interface (@kjx wanted this, I think);
yeah. I started thinking about this again, and I can see the appeal of this. If we don't want Done to turn into Nil, though, there are real advantages of being completely opaque. Every method we add to Done is a method that means Done cannot be elided by static (or dynamic) type checks
• leave Done and Object the same, but keep the two names for documentation purposes (the current situation in minigrace); or
doesn't this mean you can't filter Done's out of some stream with the obvious code?
• distinguish Object from Done by adding a marker method to Object (e.g., give graceObject a method iAmAnObject -> Boolean, and make Object demand that method too).
this goes towards the Right Thing but a marker method is a Bad Idea.
In debugging, I find it very helpful that Done has the asString method. Hence I would be unhappy to lose that.
is that because you want to compute with the string representation, or just print out the object immediately? we could make "print" do (reflexive) stuff with done and friends without they having to have asString. Or not.
but e.g. even if you do this print "name: " ++ potentiallyDoneOrAName ++ " email: " ++ potentiallyDoneOrAnEmail
(or I guess print "name: {potentiallyDoneOrAName} email: {potentiallyDoneOrAnEmail}"
should that pass a static type check?
should it be able to print "name: done " email: done" or should it crash if one of the potentials is actually done?
making print reflexive means that print would accept an argument of type "None" or "Anything" - morally the type interface { asString -> String } | None and could still print "done" if directly handed a done.
but who knows, that still may be too confusing to novices
James
@kjx wrote:
making print reflexive
Print isn't special at all — all it does is display its argument, which must be a String.
The thing that we would have to make special, if you wanted printing of done to work, is string interpolation. Right now, "{anObject}"
just interpolates anObject.asString
; it's exactly equivalent to "" ++ anObject.asString ++ ""
. Are you proposing that it be different?
@kjx wrote:
doesn't this mean you can't filter Done's out of some stream with the obvious code?
Probably; I'm not sure what the "obvious code" would be. Is it something like the following?
aStream.filter { each -> Done.matches(each).not }
As far as I can see, that has never worked, because Done
has always matched every object.
Probably; I'm not sure what the "obvious code" would be. Is it something like the following?
I can't remember.
aStream.filter { each -> Done.matches(each).not }
unless we tag Done, that won't work. Frankly, perhaps we should.
I'll give myself the benefit of the doubt and say
aStream.select { _ : Object -> ... }
the Right Thing is unlikely to be the Right Thing for "novices. I guess we should think about code snippets people might want to right, and then make sure they work if possible. In which case
aStream.filter { each -> Done.matches(each).not }
should, I guess, work - but that requires done to be tagged, and different to "object { }"
J
I now think I'm probably just wrong. What's the point of filtering out done? what does that tell you about what's left behind? absolutely nothing.
there should be at least one method that most other objects have that done doesn't. "==" perhaps?
or make done an autozygotic singleton or something. (which could unify done and Done, but done's interface would then be Type[Done])...
I thought that having all other objects understand the equality family was what "The Left and of Equals" argued against. I know that when I took it out, I exposed several bug in the compiler.
Enabling any kind of filtering of done
, or matching against Done
, would also enable done
to be used as a null — because it would enable an isDone
test.
A possible way of out of this dilemma is to eliminate done
and Done
, and make assignments return the assigned object. Mostly this is what you want anyway; I often find myself defining a pair of methods such as
method color(c) { myColor := c ; self }
method color:=(c) { myColor := c }
so that I can use the first in a chain, which I can't do with the second. In other words, it enables me to write:
def b = box.size(3@3).color(blue).filled
rather than
def b = box.size(3@3)
b.color := blue
b.filled
I do, though, still like being able to distinguish mutators from observes by making the mutators answer done
.
It's nice that we can go into the future and comment (It wouldn't print out, but the message before this currently says "kjx commented 2 hours from now").
I have never liked having assignments returning values as it encourages students to put assignments in the middle of expressions, creating code that is highly dependent on order of evaluation -- and there lies madness (channelling James!).
What we really need is something that would allow us to say the current value is really, really, done and not something extending it (e.g., something that comes from a subclass of graceObject). Exact types might help - we will need those if we really want to take SelfType seriously, but I'm not ready to propose those at this point.
Grace's order of evaluated is tightly defined, hopefully avoiding some problems.
There's always if (valueOf { x = x + 1; x }) then...
if one's feeling particularly evil.
There are a couple of thing things in here as well:
object { }
interface { }
The Done
type has to incorporate anything that could possibly be returned from an expression.
It would be odd if there were things that didn't conform to interface { }
(the only thing that may not is done
)
Michael reminded me that there is also the issue of done
being on the right - especially where primitives are concerned. Done done == 0
presumably crashes with a missing request; 0 == done
should also die, ideally with an indistinguishable error. Doing that, and not having a way to filter out done
will help. I was wrong above: programs shouldn't filter out Done
they should explicitly filter in everything else.
make done an autozygotic singleton or something. (which could unify done and Done, but done's interface would then be Type[Done])...
which I wrote earlier, and looks even wronger, because that would mean Type[Done] has the whole type and pattern interface lurking under the covers.
0 == done should also die, ideally with an indistinguishable error. Doing that, and not having a way to filter out done will help
This seems like a contradiction. If 0 == done
crashes, rather than answering false
, while 0 == <anything other than number 0>
does answer false
, then we have a way of filtering-out done
.
I was wondering about getting rid of Done
and done
and replacing them with a syntactic distinction between expressions and statements, as in Algol 60 and Pascal. That way, treating an assignment as an expression would be a syntax error, rather than a type error. It would mean distinguishing between methods that do and do not return results, which is something that no other OO languages does, as far as I know. And would significantly complicate the syntax ... which is why, I think, that Algol 68 and C abolished this distinction.
I always liked the distinction between functions and procedures, as students get very confused about something returning Done or Void or whatever. It would mean we would need to duplicate some things e.g. if..then..else expression vs statement, match expression vs. statement, which would be unfortunate.
On May 16, 2019, at 8:13 AM, Andrew Black notifications@github.com wrote:
0 == done should also die, ideally with an indistinguishable error. Doing that, and not having a way to filter out done will help
This seems like a contradiction. If 0 == done crashes, rather than answering false, while 0 == <anything other than number 0> does answer false., then we have a way of filtering-out done.
I was wondering about getting rid of Done and done and replacing them with a _syntactic` distinction between expressions and statements, as in Algol 60 and Pascal. That way, treating an assignment as an expression would be a syntax error, rather than a type error. It would mean distinguishing between methods that do and do not return results, which is something that no other OO languages does, as far as I know. And would significantly complicate the syntax ... which is why, I think, that Algol 68 and C abolished this distinction.
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/gracelang/language/issues/161?email_source=notifications&email_token=AAN2D6RWNO3ZYGQHX2HAUHDPVV22DA5CNFSM4ES625C2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGODVSEEOY#issuecomment-493109819, or mute the thread https://github.com/notifications/unsubscribe-auth/AAN2D6QGMOU6ZVBAXIFZVO3PVV22DANCNFSM4ES625CQ.
This seems like a contradiction. If
0 == done
crashes, rather than answeringfalse
, while0 == <anything other than number 0>
does answerfalse
, then we have a way of filtering-outdone
.
we already have a way that's nearly as good: done.asString
(or done.anything
)
If done has an asString, then we can see if it's "done" :-)
If not, then this will crash and it's probably done.
More usefully, we can filter other things out of a list: what's left is 'done':
match (potentiallyDone)
case { _ : Object -> print "Not Done" }
else { print "done }
If you think about any possible code we could write using done, that code can only run if it never makes a request of done - it can take in done as an argument, return done as a result, but not request anything - and I guess do this kind of match. Which is why I think primitives should have that behaviour too.
I thought of a possible compromise; I can't decide if it's a brilliant solution or a horrible hack (or, perhaps, an 'orrible 'ack, since I'm in Montréal).
done
no methods at all. This will please @kjx.done
respond to asString
and asDebugString
, by using the reflection interface (specifically, the onNoSuchMethodDo
handler. This will satisfy @apblack, who, when actually programming in Grace, sometimes forgets to return a result at the end of a method.If we put some "Done inference" into the compiler, then we probably can avoid ever generating done
at runtime.
By "Done inference" I mean generating a static error if any method whose heading does not say -> Done
has a return without a value, or terminates with a statement that returns Done
. Once we are doing that, we may as well put full type inference into the language. This would also effectively force programmers into adding return-type annotations.
Now I'm going to switch hats entirely, and argue in favor of making Done
and Object
the same.
As @kjx pointed out above, if done
has no methods, one can write an isDone
predicate as follows:
method isDone(x) -> Boolean {
Object.matches(x).not
}
and then proceed to use done
like nil
. I think that the only was to avoid this is to leave Done
and Object
the same — so this is a positive argument favour of resolution 2 above.
Two comments:
Finally, I think we shouldn't worry about making it impossible for students to do bad things. The best we can hope for is to make it easier to do things the right way. Our students are creative about developing awful code.
I thought of a possible compromise; I can't decide if it's a brilliant solution or a horrible hack (or, perhaps, an 'orrible 'ack, since I'm in Montréal).
un 'ack 'orrible, surely?
• We give done no methods at all. This will please @kjx.
sure.
• Implementations can nevertheless make done respond to asString and asDebugString, by using the reflection interface (specifically, the onNoSuchMethodDo handler. This will satisfy @apblack, who, when actually programming in Grace, sometimes forgets to return a result at the end of a method.
this, on the other hand, is evil. (or: shows how the dynamic type system should be co-algebraic aka object-oriented, not just the underlying system - i.e. an object's type should be the set of messages to which it will respond whether directly, reflexively, or whatever.
Making type Done = interface { }
and giving done methods is rather more respectable.
If we put some "Done inference" into the compiler, then we probably can avoid ever generating done at runtime.
runtime is a separate issue. Besides, what's generating done?
By "Done inference" I mean generating a static error if any method whose heading does not say -> Done has a return without a value, or terminates with a statement that returns Done. Once we are doing that, we may as well put full type inference into the language.
this is tricker. "Done checking" in a non-gradually-typed language is rather more straightforward than full type checking.
Done and Object should be the same Now I'm going to switch hats entirely, and argue in favor of making Done and Object the same.
This has the distinct advantage that we'd have an object done
and type Object
.
We wouldn't have a type Done
and perhaps not even graceObject
, unifying done
with graceObject
It's not clear this really fixes the problem, though. I don't think the problem really is "avoidable", unless what you want, what you really really want, is only ever to call asString on an object. If you ever want to do anything else, you can just do a type test on that other thing.
The point must be to ensure that if you've got done
polluting something, you'd better write the type 'Spaceship | Done' (or for that matter Spaceship | Object
) rather than just Spaceship
. That changes the question from how do we stop people handling done
explicitly to _how do we make clear when people need to handle done
explicitly. (or nil
or null
or whatever).
Programmers can always make their own nil - indeed, isn't that the preferred solution?
def myNil = object { var rat is public } type MyNil = interface { rat } method isNil(x) { MyNil.matches(x) }
what I really, want what I really really want is people to make their own nils
, put an iaNil
method in both myNil
and Spaceship
and then slam those methods down so myNil
turns into a real null object...
zig-a-zig ahh
When I'm programming in Grace, all I really need for done is it to respond to asString or asDebugString.
Is that what you really, really, want?
I guess I want to be able to pass done to print
(unfortunately, often as part of interpolation) and get something sensible without it crashing. If a done
somehow floats into another variable, I don't think I want it to return "done"
as aString. (asDebugString
perhaps, but...).
(see earlier discussion https://github.com/gracelang/language/issues/161#issuecomment-427750681)
I wonder if we should steal anything idea from Python:- something like "{{x}}"
results in interpolating to '"x = 42"' for suitable values of 42. If you want to debug, you'd write "{{foo}}"
and get done handling and who knows initialisation handling and whatever else. If you want to print output, you use "{x}" and you get what you get...
Yes, we do want people to make their own Singleton objects. There is even a singleton factory in standardGrace to help them. The idea is that they then give those objects all the necessary behaviour so that they never have to make a type test. Type tests are algebraic, rather than co-algebraic. Type tests break the "Tell, don't Ask" rule. Type tests are the antithesis of OO.
Type tests are neither necessary nor sufficient to identify such singletons. What is necessary and sufficient is an identity test.
def myNil = object {
use identityEquality
method asString { "myNil" }
}
method isMyNil(x) { myNil == x }
When I spoke about "generating done", what I was referring to was a method request used as an expression that actually answers a done
object. If we can detect all of these places statically, then any such program can be made illegal, and so done
would never actually exist. Obviously, methods used as statements can return done, but that done
is immediately dropped on the floor, so never need be returned.
Static type systems are subject to Gödel's incompleteness theorem. There is no hope of determining statically whether an object responds to a particular message if an object's response to a message can be set by arbitrary executable code (as it is in the reflection interface).
On May 27, 2019, at 15:04, kjx notifications@github.com wrote:
un 'ack 'orrible, surely?
Patty says: shouldn’t that be “un aque horrible”
what I was referring to was a method used as an expression that actually answers a done object. If we can detect all of these places statically, then any such program can be made illegal, and so done would never actually exist
a static type checker would necessarily approximate all these places.
the philosophical question is: do you raise the error when a 'done' is referenced, or when it receives a request. Ha! this is literally the tree falling in the forest with no-one there to hear.
We already have a value that raises an error whenever it is referenced: undefined
. This is the "value" that is used to initialise uninitialized variables. The very act of binding to an undefined value is an error.
So, yes, this ia a possible alternative. Make assignments and methods that currently return done
instead return something that is like undefined
. The act of attempting to assign this return value to a variable, or use it as a target or argument to a request, would be an error — as happens with undefined
at present. This is perhaps the "right" answer; all we need is someone who knows a little about flow analysis to educate our very ignorant implementor about how to do it, with reasonable efficiency. (The current checks for undefined
are pretty pathetic — they happen much more often than is reasonable.)
With this semantics, and
method someCommand {
x := 0
}
the assignment
var y := someCommand
would blow up, essentially with an "undefined" exception (although we would probably want to call it something else), whereas
someCommand
by itself (i.e., making no use of the answer) would be fine.
Actually, this new value — lets call it noValue
— would be the converse of undefined
. Whereas there are no expressions that have value undefined
, but variables may be undefined
, there would be many expressions that have noValue
(any assignment, for example), but variables could never take on noValue
(because we would check before every assignment to ensure that they do not).
I suppose that we could call noValue
void
, but that seems so much less cool.
Ha! this is literally the tree falling in the forest with no-one there to hear.
I disagree. There is a huge difference between undefined
in Grace, which blows up as soon as you try to assign it or pass it as an argument, and undefined
in JavaScript, where you can pass it around to your heart's content and nothing bad happens until you send it a message. The JavaScript approach makes possible a whole style of programming that is infeasible in Grace, and a whole host of errors that are impossible in Grace. (These are the errors when you find that a variable is undefined
and you have to track down when undefined
was assigned to it, by whom, and why.)
Trust me on this: we do not want to go there!
After a discussion: we (@apblack and @kjx) figured out that distinguishing between procedure methods and function methods, or stopping done
from flowing into contexts where it may be used (rather than when it actually is used) stops us from writing code that is parametric over whether or not it returns a value.
For example, the code below could only work for lambdas that return values, not that return done, even if the return value from this apply
method is never used.
var counter := 0
class countCombinator(lambda) {
method apply(x) { counter := counter + 1; lambda.apply(x) }
}
That is definitely a disadvantage ...
I just re-read my comment above:
Trust me on this: we do not want to go there!
and I realized that this is exactly where were are in Grace with done
. If I forget to return a value in a method somewhere, then a done
shows up in some other object a while later, and I have to track down which method is broken.
A type-checker, fortunately, will catch such an error. At least: it will do so provided that done
does not conform to the declared type of the method.
The problem with making done
have type Object
is that it might then so conform. This was the motivation for my suggestion that done
not have type Object
.
'done' should not have type Object.
def done = object { inherit done } //otherwise it gets graceObject
type Done = interface { }
I've just re-read this thread while I should have been preparing my final lecture There is no easy solution, but I think that it might be worth trying (in minigrace) the approach of making done
have no methods at all. So
type Done = interface {}
I think that it might also make sense for Unknown
not_ to match done
, so that if one tries to assign done
to a variable that has no type annotation, it would be a type error! (If you really want to put done
in a variable, you could give that variable the type Done
).
How does this address @apblack's oft-repeated point about the importance of being able to convert done
to a string so that errors show up? Well, I've realized that the implementation already has to guard against asString
being broken. For example, there are already two places where the runtime will request asString
, and if that breaks, use something like "(without working asString method)". I think that it would be easy to get done.asString
to produce a message like "built-in object done has no method asString
".
Incidentally, Python has a handy peg to hang this on. While every Python object is supposed to have a __repr__
method (which is a more like the Fortress' asEvalString
than asDebugString
), the convention is that programmers don't request obj.__repr__()
directly: they write repr(obj)
instead; repr
is a global function that calls __repr__
. But that funciton can do other things too, in the case that __repr__
fails or does not exist. Something similar happens with obj.__next__
and next(obj)
; the former raises the StopIteration
exception, while the latter can be parametrized to return a sentinel object.
I'm not proposing this solution for Grace, which is object-based, not function-based, but it would be in interesting experiment to see if we can maintain good error behaviour with a done
that has no methods.
I would like feedback, specifically, on the idea of Unknown
not matching done
. Our implementor wouldn't like it (currently, type-checks agains Unknown
are elided, because they always pass).
Perhaps a compromise would be to let Done have asDebugString, but not asString. I think that would satisfy some of my concerns about being able to extract debug information. I don’t have strong feeling about Unknown not matching Done, but lean toward not matching. If Done has asDebugString then Unknown could match any type extending Object (and hence not Done).
Kim
On May 21, 2020, at 2:23 PM, Andrew Black notifications@github.com wrote:
I've just re-read this thread while I should have been preparing my final lecture There is no easy solution, but I think that it might be worth trying (in minigrace) the approach of making done have no methods at all. So
type Done = interface {} I think that it might also make sense for Unknown not_ to match done, so that if one tries to assign done to a variable that has no type annotation, it would be a type error! (If you really want to put done in a variable, you could give that variable the type Done).
How does this address @apblack https://github.com/apblack's oft-repeated point about the importance of being able to convert done to a string so that errors show up? Well, I've realized that the implementation already has to guard against asString being broken. For example, there are already two places where the runtime will request asString, and if that breaks, use something like "(without working asString method)". I think that it would be easy to get done.asString to produce a message like "built-in object done has no method asString".
Incidentally, Python has a handy peg to hang this on. While every Python object is supposed to have a repr method (which is a more like the Fortress' asEvalString than asDebugString), the convention is that programmers don't request obj.repr() directly: they write repr(obj) instead; repr is a global function that calls repr. But that funciton can do other things too, in the case that repr fails or does not exist. Something similar happens with obj.next and next(obj); the former raises the StopIteration exception, while the latter can be parametrized to return a sentinel object.
I'm not proposing this solution for Grace, which is object-based, not function-based, but it would be in interesting experiment to see if we can maintain good error behaviour with a done that has no methods.
I would like feedback, specifically, on the idea of Unknown not matching done. Our implementor wouldn't like it (currently, type-checks agains Unknown are elided, because they always pass).
— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/gracelang/language/issues/161#issuecomment-632353443, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAN2D6T2KYQCAJOJSC4XDHTRSWLTTANCNFSM4ES625CQ.
We agreed a long time ago that assignments should return
done
, and thus that any method that has an assignment as its final action would also returndone
.I presume that
done
is an object, and that it's unique. We also agreed thatdone
had no interesting methods (I want it to haveasString
for pragmatic reasons), but importantly it does not have==
or≠
. If it did, thendone
would just be another four-letter word fornull
.When
Object
had more methods, such as==
,done
was not anObject
, which was nice. But now, the type ofdone
, which we have been writing asDone
, is the same asObject
. This is unfortunate, because saying that a method returnsDone
used to be a way of clearly saying that it has no interesting result, whereas saying that it returnsObject
seems to imply the opposite.I'm not sure what to do about this. We can of course continue writing method signatures with
Done
in them, and ignore the fact thatDone
≡Object
, using the name for documentation purposes. But that doesn't seem wholly honest.