gracelang / language

Design of the Grace language and its libraries
GNU General Public License v2.0
6 stars 1 forks source link

What's "done"? #161

Open apblack opened 6 years ago

apblack commented 6 years ago

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 return done.

I presume that done is an object, and that it's unique. We also agreed that done had no interesting methods (I want it to have asString for pragmatic reasons), but importantly it does not have == or . If it did, then done would just be another four-letter word for null.

When Object had more methods, such as ==, done was not an Object, which was nice. But now, the type of done, which we have been writing as Done, is the same as Object. This is unfortunate, because saying that a method returns Done used to be a way of clearly saying that it has no interesting result, whereas saying that it returns Object 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 that DoneObject, using the name for documentation purposes. But that doesn't seem wholly honest.

KimBruce commented 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).

apblack commented 6 years ago

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.

apblack commented 6 years ago

As I wrote in email on 19 August ("Re: odd error"), I can see three resolutions to this issue.

  1. Make Done the empty interface (@kjx wanted this, I think);
  2. leave Done and Object the same, but keep the two names for documentation purposes (the current situation in minigrace); or
  3. 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).
KimBruce commented 6 years ago

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.

kjx commented 6 years ago

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

apblack commented 6 years ago

@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?

apblack commented 5 years ago

@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.

kjx commented 5 years ago

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

kjx commented 5 years ago

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])...

apblack commented 5 years ago

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.

KimBruce commented 5 years ago

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.

kjx commented 5 years ago

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.

apblack commented 5 years ago

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.

KimBruce commented 5 years ago

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.

kjx commented 5 years ago

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.

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.

apblack commented 5 years ago

How to make Done and Object different

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).

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.

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.

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.

KimBruce commented 5 years ago

Two comments:

  1. When I'm programming in Grace, all I really need for done is it to respond to asString or asDebugString. Thus I'd be happy with the first solution.
  2. I think it is bad style to write "return result" at the end of a method. It should just be "result". I teach my students that return is there only to change the flow of control -- e.g., in the middle of a method you determine the answer in an intermediate result and can return immediately.

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.

kjx commented 5 years ago

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

kjx commented 5 years ago

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...

apblack commented 5 years ago

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).

apblack commented 5 years ago

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”

kjx commented 5 years ago

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.

apblack commented 5 years ago

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!

kjx commented 5 years ago

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) }
}
KimBruce commented 5 years ago

That is definitely a disadvantage ...

apblack commented 5 years ago

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.

kjx commented 5 years ago

'done' should not have type Object.

def done = object { inherit done }   //otherwise it gets graceObject
type Done = interface { }
apblack commented 4 years ago

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).

KimBruce commented 4 years ago

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.