Skip to content
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

Syntax for once methods #41

Closed
apblack opened this issue Feb 14, 2016 · 25 comments
Closed

Syntax for once methods #41

apblack opened this issue Feb 14, 2016 · 25 comments
Labels

Comments

@apblack
Copy link
Contributor

apblack commented Feb 14, 2016

We agreed to add once methods. But with what syntax? And are they allowed to have parameters (which would imply a memo table).

@kjx
Copy link
Contributor

kjx commented Feb 14, 2016

Isn't this covered here #31?

I suggested: def x is lazy = 3 a while ago.

But with what syntax?

Issue #31 lists other options; this is my current preference

And are they allowed to have parameters (which would imply a memo table).

nope. That would also require every object used as an argument to a lazy thing to have == and hash

(argh. Thought I wrote this earlier but it seems to have got lost?)

@apblack
Copy link
Contributor Author

apblack commented Feb 14, 2016

Sorry, you are right, this is covered in #31. My bad.

The problem with def x is lazy = 3 is that the use case for once is that the value to be calculated once is not likely to be something simple like 3, but something complicated and expensive. So a more realistic example would be something like:

def x is lazy, public = {
      long algorithm declaring names
      and calculating expressions and eventually
      resulting in a return value
}.apply

which doesn't look so good. It also has the problem of using an annotation to change the semantics. I think that

once method x {
      long algorithm declaring names
      and calculating expressions and eventually
      resulting in a return value
}

is more realistic. (We could also make it

method x is memoized {
      long algorithm declaring names
      and calculating expressions and eventually
      resulting in a return value
}

if we decide to quit worrying about annotations changing the semantics.)

@kjx
Copy link
Contributor

kjx commented Feb 14, 2016

which doesn't look so good

but this isn't so bad (thanks to Guy Stele and Niklaus Wirth):

def x is lazy, public = begin {
      long algorithm declaring names
      and calculating expressions and eventually
      resulting in a return value
}

We don't have begin in the library, but we probably should, as normal defs have the same problem.

It also has the problem of using an annotation to change the semantics

I'd rather have a coherent, dynamic (non-rewriting based) semantics for annotations, rather than having them not change the semantics. But we don't have any "doubled-up keywords" (aka space-sensitive keywords) so if we don't like def is lazy why not go the full Eiffel:

once x {
      long algorithm declaring names
      and calculating expressions and eventually
      resulting in a return value
}

My preferences would be **def is lazy* over once, because I'd rather describe it as a special sort of def (that is just initialised at a different time to normal defs) rather than a special kind of method that is magically run once. Hmm: I also realise there is an important semantic difference: what does return do in these initialisers? In a def it would try to return from method enclosing the def, in a *once it would presumably give the memorised value...

@apblack
Copy link
Contributor Author

apblack commented Feb 15, 2016

You are right, it would be useful to have something equivalent to your begin method in the library.
I would suggest calling it valOf (shades of BCPL), or, more Gracefully, valueOf

My preference is not to use the def syntax for this, for one, two, three ... reasons.

First, there is an open issue (#33) here about re-purposing def to be a manifest declaration, so making it also do once methods seems too confusing.

Second, traits can't contain defs. But we do want them to contain once methods.

There is also a third reason, although it's a weak one — If one wants to illustrate what a once does by re-writing. It will re-write to a method with a hidden variable or two.

The fourth reason is that some time in the future, I (or my successors and assigns) might prevail upon you to allow onces to have parameters — which means implementing memoization in the compiler, and requiring that the parameters undersatnd == and hash. The method syntax will then generalize nicely.

This gives me another reason to like the syntax: method wombat is memoized { ... }.

@kjx
Copy link
Contributor

kjx commented Feb 16, 2016

You are right, it would be useful to have something equivalent to your begin method in the library.
I would suggest calling it valOf (shades of BCPL), or, more Gracefully, valueOf

BCPL would be valof. I wondered about just value (but we probably don't want to steal value as something that is almost a keyword).

BUT: This counts as issue-jacking. Andrew has opened issue #52 for just this point.

@kjx
Copy link
Contributor

kjx commented Feb 16, 2016

  • mainfest - needs more discussion at definition of manifest #47. Methods needs to be manifest or manifestable or something
  • once in traits - do we really want once in traits? Doesn't that mean people will just once once in traits where def would make as much sense, but can't use def because they're not allowed?
  • rewriting - yes that's a weak argument :-)
  • memoisation - that'll take a while - and is a YUUGGEEEEE change to the language, semantics, etc. The rationale for lazy def is, I think, to do with initialisation order, and types. General memorisation is different
  • I'd like to mention reference languages: Scala is lazy def; Newspeak is a form of "def"; Eiffel is method-like.

more to the point, can't we do this today, and isn't it enough:

def memo = dictionary<Tuple<City,Salesman>> 

method travel( city, salesman ) { 
   memo.at( tuple(city) and(salesman) ) IfAbsentPut { _ -> 
    ///computation involving city and salesman 
   } 
}

@apblack
Copy link
Contributor Author

apblack commented Feb 16, 2016

once in traits - do we really want once in traits? Doesn't that mean people will just once once in traits where def would make as much sense, but can't use def because they're not allowed?

Yes, that's right. It's because object initialization is broken that we need once.

@kjx
Copy link
Contributor

kjx commented Feb 16, 2016

Yes, that's right. It's because object initialization is broken that we need once

or we could fix object initialisation.

I don't know of any initialisation scheme for imperative languages that does what we want.
(I don't know e.g. what Rust or Kotlin or Ceylon does).
Once can do stuff wrt mutual recursion that very few other schemes can do, and also supports lazy initialisation quite nicely.

@kjx
Copy link
Contributor

kjx commented Feb 16, 2016

more to the point, can't we do this today, and isn't it enough:

was talking about memorization of functions with parameters. To do that for initialisation (e.g for grammars done in parser combinators) requires explicit proxies - we can't get the timing for that right easily without an explicit extra level of indirection. Memomising on a large scale, I don't mind that indirection so much.

@apblack
Copy link
Contributor Author

apblack commented Feb 16, 2016

For non-recursive, non-compute intensive cases, I suspect that O'Caml does the right thing — computes initial values before entering the scope of the object.

For the rest: yes, we can program up lazy initialization. We've been doing that since at least Smalltalk in 1980. I have to say, though, that I think that Bertrand is right: once captures a fundamental idiom. It deserves to be in the language. No, I would not teach it to novices in the first quarter. But I would certainly use it in my libraries.

This post, let me remind you, is not about whether we should have it: we already agreed on that, and I haven't heard any objections. It's about what syntax to choose.

@kjx
Copy link
Contributor

kjx commented Feb 16, 2016

It's not just "But with what syntax?" but also "And are they allowed to have parameters" and "are they allowed in traits". Eiffel oncers also serve the purpose of being thread local, global, or per object! (which is NOT the default!) Do we need to consider that.

@apblack
Copy link
Contributor Author

apblack commented Feb 16, 2016

We already agreed on no parameters for the moment. We may want to revisit it at some future time, but for this round of the spec: no parameters.

Yes, they are allowed in traits. Why ever not? We already agreed that "hidden" state is OK in a trait, and this is what once requires. We don't have to decide on local or global, because in Grace, both of these are per-object for some object — one place where we have a better design than Eiffel! As for thread local: yes, we will have it think about that someday.

Any other red herrings?

@kjx
Copy link
Contributor

kjx commented Feb 16, 2016

We already agreed on no parameters for the moment. We may want to revisit it at some future time, but for this round of the spec: no parameters.

I'm happy with that.

Yes, they are allowed in traits. Why ever not?

I don't know. It's not obvious. We have to spec it.

As for thread local: yes, we will have it think about that someday.

It's not a red herring; it's something we have to think about given the Eiffel design. I also agree we don't need do that, we can use another mechanism if we care. But we should consider it a bit. Which we've done.

Syntactically: my preferences currently are (from most preferred)

  1. def ... is lazy
  2. once
  3. method is once
  4. lazy def
  5. once method

@apblack
Copy link
Contributor Author

apblack commented Feb 16, 2016

For your number 2: is once an alternative to method, or an alternative to begin, as in Eiffel?
Well yes, I know that we spell begin {, and once ... } would be pretty weird, but I guess I'm asking how you see 2. once being used.

@kjx
Copy link
Contributor

kjx commented Feb 16, 2016

as above:

once x {
      long algorithm declaring names
      and calculating expressions and eventually
      resulting in a return value
}

I still prefer def is lazy. I half want to stick an = in there, and drop the { } (because why add unnecessary { }s that aren't block delimiters) but again that's why I prefer 'def is lazy' --- and because I'd rather sell them as def-like than method-like

I don't like once method or lazy def because we don't have those kinds of adjectival keywords in Grace.

@apblack
Copy link
Contributor Author

apblack commented Feb 16, 2016

So the syntax is like a method, with the once keyword replacing method ?

@kjx
Copy link
Contributor

kjx commented Feb 16, 2016

that's my second preference

@apblack
Copy link
Contributor Author

apblack commented Feb 16, 2016

I could go with that. My preferences would be (in order)

  1. method is lazy
  2. once method
  3. once

so, by minimizing the root-mean-square-dis-preference, we converge on your (2) and my (3).

@kjx kjx added the 2:soon label Feb 17, 2016
@kjx kjx added 3:later and removed 2:soon labels Mar 8, 2016
@apblack
Copy link
Contributor Author

apblack commented May 21, 2016

On reflection, I prefer once method to once by itself, because it makes it clear that this construct is a method. We do currently have factory method, although that will soon be removed. When taking about them to a class of students, I want to say "once methods" rather than "onces", which isn't even a word.

A reason to prefer once method over def ... is lazy is that refs are not allowed in traits. One of the reasons to have once methods is to put them in traits

@kjx
Copy link
Contributor

kjx commented May 21, 2016

I don't think we're done with this design yet. In particular, I'm now unsure being lazy/once isn't quite enough, if you want to e.g. create cyclic structures, you need some kind of future or placeholder.

The point is that we certainly need to build those kinds of structures with types: better to have one mechanism strong enough to do that too.

@apblack
Copy link
Contributor Author

apblack commented May 21, 2016

We may well not be finished, but cyclic structures don't need even once methods. Here is a litttle example of infinite (cyclic) lists that runs today in minigrace:

def one = object {
    method next { two }
    method value { 1 }
}

def two = object {
    method next { one }
    method value { 2 }
}

method show(start) {
    var n := start
    var output := ""
    repeat 10 times {
        output := output ++ "{n.value}, "
        n := n.next
    }
    print(output ++ " …\n")
}

show(one)
show(two)

The output is

1, 2, 1, 2, 1, 2, 1, 2, 1, 2,  …

2, 1, 2, 1, 2, 1, 2, 1, 2, 1,  …

as you would expect.

We already have a way of writing types that allows cyclic structures — type declarations and type literals. We really do need types to be special, because they need to meet restrictions that ensure that they are not only computable but bounded.

I agree entirely that we want minimal mechanisms. What do you see a "future" or "placeholder" doing that a once method doesn't do?

@kjx
Copy link
Contributor

kjx commented May 22, 2016

We already have a way of writing types that allows cyclic structures — type declarations and type literals. We really do need types to be special, because they need to meet restrictions that ensure that they are not only computable but bounded.

The question about types I guess is: how special? Yes, we can use methods or blocks to work around things (that's what futures and to a lesser extent placeholders do). The combinator parsers, for example does this:

 def primaryExpression = rule { literal | listOfOuters | implicitSelfRequest | parenExpression }  
 def parenExpression = rule { lParen ~ rep1sep(drop(opt(ws)) ~ expression, semicolon) ~ rParen } 

where that "rule" is pretty much a custom proxy that lets me build up the cyclic structure - expression links back to primaryExpression of course.

Hmm: then again types have to be more method-like because they can take (generic) arguments where these things don't (so far). Which just gets to me not understanding all the issues in this space right now (except that I don't like once method because we have no other sequences of keywords like that in Grace so far).

@apblack
Copy link
Contributor Author

apblack commented May 22, 2016

I'm aware of the rule hack in combinator parsers; it struck me as more-or-less a facsimilie of what Smalltalk's PetitParser framework does with meta-programming. In Smalltalk, it's important because otherwise one would have to write

self.literal | self.listOfOuters | self.implicitSelfRequest | self.parenExpression

so PetitParser uses metaprogramming to declare an instance variable corresponding to each method, and initialize them with proxies, which get eliminated lazily.

But Grace doesn't have that problem: we can request methods without self. So you could have written:

method primaryExpression  { literal | listOfOuters | implicitSelfRequest | parenExpression }  
method parenExpression { lParen ~ rep1sep(drop(opt(ws)) ~ expression, semicolon) ~ rParen } 

Why didn't you? For efficiency reasons: you didn't want to build the same parser over again each time you used it. You want to build it once, the first time that it is needed, and cache it.

That's exactly what once method does. What else do you need?

The issue that I see with types is that, even if we define a type to be no more than a special type of object with certain methods, we probably want a type to be less than that. That's why we need a type syntax, and why we need to say that programmer's can't create types out of whole cloth (that is, out of methods and defs), but must use the type syntax. Otherwise, they could define a type where the pattern of method names that one finds as one explores the type is the same as the digits of π ... it's computable, but type-checking never terminates, because the type is represented by an infinite, non-cyclic structure.

@kjx
Copy link
Contributor

kjx commented May 23, 2016

So you could have written:

method primaryExpression  { literal | listOfOuters | implicitSelfRequest | parenExpression }  
method parenExpression { lParen ~ rep1sep(drop(opt(ws)) ~ expression, semicolon) ~ rParen } 

Why didn't you?

Umm, I think because it would loop? calling primary expression would call parenExpression would call expression would call primaryExpression? That's what happens in Kernan:

import "parsers2" as parsers
Getting Parsers
done
inherit parsers.exports
Got Parsers
import "grammar" as grammar
Got Parsers
inherit grammar.exports

method primary { literal | parenExpression }
method literal { graceIdentifierParser }
method parenExpression { lParen ~ primary ~ rParen }
method lParen { symbol "(" }
method rParen { symbol ")" }

primary
Stack overflow in unmanaged: IP: 0x297289, fault addr: 0xbf7b6fcc

Unhanded Exception:

StackOverflowException
Stack overflow in unmanaged: IP: 0x6b11c, fault addr: 0xbf7b59bc
Stack overflow in unmanaged: IP: 0x105b3c, fault addr: 0xbf7b5edc
[ERROR] FATAL UNHANDLED EXCEPTION: Nested exception detected.

What's worse, it's not clear that making this a once-ler or lazy or whatever fixes things -- because the call chain gets back to e.g. primary or wheverelse you start before that method has finished executing, i.e. before the cache has been set (because you haven't finishing computing the value yet). And that's where placeholders or futures or proxies work, because you immediately return a proxy, and can snap the links one step at a time, when the proxies are actually used. Either using methods, once-lers or defs, each of them is immediately initialised to the proxy, and only when its is used, the proxies are snapped one link at a time...

@apblack
Copy link
Contributor Author

apblack commented Aug 2, 2019

I’m closing this because once methods have been added, with the syntax once method.

I had completely forgotten the problem with @kjx’s Combinator parser. It seems to me that the problem must lie in the definition of ~ being too eager: it needs to execute its right-hand argument only when the left-hand argument has succeeded. The grammar should not loop unless it is left-recursive. Now that once methods work, I should try this out.

@apblack apblack closed this as completed Aug 2, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants