eclipse-archived / ceylon

The Ceylon compiler, language module, and command line tools
http://ceylon-lang.org
Apache License 2.0
399 stars 62 forks source link

Attribute lazy initialization #3544

Closed CeylonMigrationBot closed 7 years ago

CeylonMigrationBot commented 12 years ago

[@matejonnet] Looking for a shorter way to initialize an object attribute on a first use, and ability to use immutable attribute instead of variable

variable Foo? foo := null;
Foo getFoo() {
    if (exists f = foo) {
        return f;
    }
    foo := createFoo();
    if (exists f = foo) {
        return f;
    }
    throw;
}

I would like to write this as:

Foo? foo;
Foo getFoo() {
    if (!exists foo) {
        foo = createFoo();
    }
    return foo;
}

[Migrated from ceylon/ceylon-spec#438]

CeylonMigrationBot commented 12 years ago

[@matejonnet] Or even shorter if it is possible to do something like this:

Foo foo ?= createFoo();

"?=" will cause that createFoo() is called on first usage of foo.

CeylonMigrationBot commented 12 years ago

[@RossTate] The non-variable version is bad cuz you're accessing a value before it's been initialized.

variable Foo? foo := null;
Foo getFoo() {
    if (exists f = foo) {
        return f;
    }
    foo := createFoo();
    return foo;
}

would be bad because between foo := createFoo(); and return foo; a concurrently running program could have changed the value of foo to null.

Lazy<Foo> foo = lazy createFoo(); seems a better solution.

CeylonMigrationBot commented 12 years ago

[@luolong] +1 for this syntax:

Foo foo ?= createFoo();
CeylonMigrationBot commented 11 years ago

[@gavinking] So, @matejonnet has a point here. The simplest I was able to get was the following:

Integer computeVal() => ... ;

variable Integer? lazyVal:=null;

shared Integer val {
    if (exists val=lazyVal) {
        return val;
    }
    else {
        value val=computeVal();
        lazyVal:=val;
        return val;
    }
}

which sorta took me by surprise. It's a bit better than what @matejonnet writes above, but it's still unacceptably verbose.

The big problem here is that, ages ago, @FroMage asked me to change the type of assignment expressions like lazyVal:=computeVal() from the RHS type (in this case, Integer) to the LHS type (in this case, Integer?) to make things easier on the Java backend. If we had not made this change, then the following code would work:

Integer computeVal() => ... ;

variable Integer? lazyVal:=null;

shared Integer val =>
        lazyVal else lazyVal:=computeVal();

Which I think would be acceptable, no?

@FroMage WDYT, can we change this back?

CeylonMigrationBot commented 11 years ago

[@gavinking] @luolong The following would certainly work:

Integer computeVal() => ... ;

variable Integer? lazyVal:=null;

shared Integer val =>
        lazyVal?=computeVal();

where ?= is an operator that evaluates the RHS and assigns it to the LHS iff the LHS is null. But it's more cryptic and not much less verbose than lazyVal else lazyVal:=computeVal().

CeylonMigrationBot commented 11 years ago

[@FroMage] I'd like a way to define lazy memoised attributes, I've already made that clear.

Perhaps memoised Integer val => computeVal() would be enough, though memoised doesn't speak to most people, but using lazy here wouldn't imply that the value is only computed once, since => is already lazy. Perhaps cached?

I think that ?= is not useful outside of this use-case (lazy memoised attributes) (though feel free to prove me wrong) and that this particular use-case still has too much boilerplate, even Gavin's solution, compared to:

Integer computeVal() => ... ;

cached shared Integer val => computeVal();

I mean, if we introduce ?= only for this use-case, we should fix this use-case better than half-way.

CeylonMigrationBot commented 11 years ago

[@quintesse] once ?

CeylonMigrationBot commented 11 years ago

[@gavinking] FTR, I'm not advocating ?=. I'm advocating we fix the type of assignment expressions back to what it should be!

I'm also not against eventually introducing a Fantom-style once annotation, but I don't want to do it in Ceylon 1.0.

CeylonMigrationBot commented 11 years ago

[@FroMage] Fair enough, but changing the type of := means making it more expensive when compiled to Java. I'm not sure that's worth it.

CeylonMigrationBot commented 11 years ago

[@gavinking]

Fair enough, but changing the type of := means making it more expensive when compiled to Java.

Only when:

CeylonMigrationBot commented 11 years ago

[@FroMage] Sure.

CeylonMigrationBot commented 11 years ago

[@RossTate] Lazy actually typically means memoized. A while back I had proposed that {...} be memoized so that the singleton case could be used for (memoized) lazy arguments. You could do the same here:

Lazy foo = {compute()};

foo.eval

On Thursday, January 3, 2013, Stéphane Épardaud wrote:

Sure.

— Reply to this email directly or view it on GitHub<#3544#issuecomment-11841604>.

CeylonMigrationBot commented 11 years ago

[@gavinking] I have made that change to the type of x=y, allowing the following idiom:

variable Integer? lazyVal:=null;
shared Integer val => lazyVal else (lazyVal=computeVal());

Now reducing issue priority and shifting out of Ceylon 1.0. We can re-discuss this stuff for a future version.

CeylonMigrationBot commented 11 years ago

[@gavinking] P.S. the backend needs to be fixed to support this change to the semantics of a nested assignment operator.

CeylonMigrationBot commented 9 years ago

[@quintesse] I see this has been suggested for 1.5 but I've already encountered several times wanting something like this. Maybe we could move it a bit forward to 1.3? It doesn't seem too hard too implement but can still quite useful.

Of course Gavin's 2-liner is not too bad, but nothing can beat:

shared cached Integer val => computeVal();
CeylonMigrationBot commented 8 years ago

[@xkr47] I also vote for shared cached Integer val => computeVal();

CeylonMigrationBot commented 8 years ago

[@kelvio] Why not "shared lazy Integer val => computeVal();"? Em 30/10/2015 9:22 AM, "Jonas Berlin" notifications@github.com escreveu:

I also vote for shared cached Integer val => computeVal();

— Reply to this email directly or view it on GitHub <#3544#issuecomment-152499456>.

CeylonMigrationBot commented 8 years ago

[@xkr47] @kelvio Ok clarification; It is especially the syntax that I am happy with. Regarding the word to use for enabling it should of course make it clear that computeVal() will only be called once. Can't say which is better, you decide..

CeylonMigrationBot commented 8 years ago

[@kelvio] Got it. I think cached really makes it clear that "computeVal()" will only be called once, however, lazy makes it clear that this attribute is lazily initialized ... I'm not quite sure what the best choice.

2015-11-02 10:24 GMT-02:00 Jonas Berlin notifications@github.com:

@kelvio https://github.com/kelvio Ok clarification; It is especially the syntax that I am happy with. Regarding the word to use for enabling it should of course make it clear that computeVal() will only be called once. Can't say which is better, you decide..

— Reply to this email directly or view it on GitHub <#3544#issuecomment-153002100>.

Atenciosamente, Kelvio Matias Santos Silva.

CeylonMigrationBot commented 8 years ago

[@gavinking] I like once, like in fantom.

CeylonMigrationBot commented 8 years ago

[@quintesse] +1 on once

CeylonMigrationBot commented 8 years ago

[@bjansen] But "once" is an adverb, and you decided to use adjectives for annotations, as stated in the faq:

The word "override" is a verb, and doesn't read well when combined with other annotations. Annotations read best together when they're all adjectives.

cached makes me think that somehow the cached value could be evicted and recomputed again, whereas lazy clearly shows that the value will be computed once, the first time it's accessed. I'd use "lazy" for initialization and "cached" for data storage.

CeylonMigrationBot commented 8 years ago

[@xkr47] Somehow I feel that => already implies that the value is calculated "on request" i.e. lazily, so in some sense I think all attributes using this construct are in some sense "lazy". The difference here is that the value is only calculated ... once.. But after all, doesn't "how the calculation is done" categorize the modification we want to apply to the statement, in which case an adverb would seem appropriate? Of course one could adjectivize "once" to some weirdo oncecalculated - to which I'd have to say no thanks :)

CeylonMigrationBot commented 8 years ago

[@luolong] maybe we should borrow from the functional languages terminology and call it memoized?

CeylonMigrationBot commented 8 years ago

[@luolong] so the example would read like this:

shared memoized Integer val => computeVal();
CeylonMigrationBot commented 8 years ago

[@ncorai] +1 on once

lazy is too ambiguous and memoized too obscure for laypeople like me.

While using a verb in a Ceylon annotation seems a big no-no, I think people would accomodate the use of an adverb.

CeylonMigrationBot commented 8 years ago

[@gdejohn] :+1: for memoized.

CeylonMigrationBot commented 8 years ago

[@lukedegruchy] I vote for cached.

fwgreen commented 8 years ago

+1 for memoized

If someone unfamiliar with the concept encountered memoized in a piece of Ceylon code, they'd probably google it and the first result would be Memoization. It might be considered a Term of Art, as I've only seen it used in relation to computer science.

ghost commented 8 years ago

@fwgreen I think I prefer lazy, also like cached, would be fine with memoized, but really dislike once.

bjansen commented 8 years ago

Could we have this in the next version? It doesn't look like a big change to implement, once we all agree on a name.

ghost commented 8 years ago

@bjansen I don’t really like this feature that much. It would be convenient, but it isn’t something we can’t already do really easily.

Besides the name, there is another issue: = vs =>. When you start to think about this, you start to notice how this will break the nice regularity of the language that says = is eager and => is lazy, by adding an in‐between.

jvasileff commented 8 years ago

but it isn’t something we can’t already do really easily.

Thread safe initialization (if we go that route) would be something not currently possible without native("jvm").

bjansen commented 8 years ago

cached value a => func(); would still be lazy, but its initial result would be reused in subsequent calls.

It would be convenient

That's the whole point of this feature :)

gavinking commented 7 years ago

I propose the following syntax:

late Float pi = calculatePi();

Where I'm repurposing the late annotation to mean "initialized lazily" instead of just "initialized later". There's no ambiguity here, since currently a late field can't have an initializer, and the compiler enforces that.

Reasons why I think this is reasonable:

FroMage commented 7 years ago

+1

quintesse commented 7 years ago

Even better +1

jvasileff commented 7 years ago

Would the type checker produce an error if you tried to assign to a specified late value? e.g.

shared late Float pi = calculatePi();
shared void run() {
    pi = 22.0/7; // error here?
}
ghost commented 7 years ago

I really dislike this idea. It seems to me like the late annotation is being used here for two completely different things simply because “it isn’t ambiguous”.

Either way, I don’t think we can have this feature without drastically changing the meaning of either = or =>, that’s why I really dislike it. If you guys introduced a new symbol for “memoized assignment” (maybe ~>) instead of an annotation I wouldn’t mind. But even then it might be a bad idea, because:

Again, it’s not particularly hard to overcome the inexistence of this feature:

variable Integer? lazyVal = null;
shared Integer val => lazyVal else (lazyVal = computeVal());
ghost commented 7 years ago

By the way, if this was something I needed all the time, I wouldn’t mind a little bit of irregularity. But this is something I’ve never actually needed.

gavinking commented 7 years ago

Would the type checker produce an error if you tried to assign to a specified late value?

Well, that's something I was wondering about, and I'm not sure, but I guess I think it should be an error, at least for shared, non-variable fields. But yeah, that's something that needs thinking-through.

quintesse commented 7 years ago

Btw, this use of late we'll be able to use everywhere, right? I mean, even locally and such.

lucaswerkmeister commented 7 years ago

late fields can already be variable (and non-variable ones can only be assigned once), so I’d say disallowing that for non-variable late fields would be consistent.

Another question:

T printIt<T>(T t) { print(t); return t; }
late Integer i = printIt(1);
shared void run() {
    i = 2;
    value j = i;
}

Does this print something? That is, if the value is assigned before ever being read, is the lazy initializer evaluated?

jvasileff commented 7 years ago

Btw, this use of late we'll be able to use everywhere, right? I mean, even locally and such

+1 on being able have memoized initialization for locals

gavinking commented 7 years ago

@Zambonifofex

I really dislike this idea. It seems to me like the late annotation is being used here for two completely different things simply because “it isn’t ambiguous”.

It might "seem" like that to you, but if you expect to be able to convince anybody else that these are two "completely different" things, then you need to present us with an argument for how and why they are semantically distinct and exhibit very different behaviors.

I've already presented a sketch of an argument for how the behavior is very semantically similar. How is my argument incorrect? What differences am I glossing over?

gavinking commented 7 years ago

late fields can already be variable (and non-variable ones can only be assigned once), so I’d say disallowing that for non-variable late fields would be consistent.

Probably, yes.

gavinking commented 7 years ago

Btw, this use of late we'll be able to use everywhere, right? I mean, even locally and such.

I guess. But we might not need that immediately.

gavinking commented 7 years ago

@Zambonifofex By the way, to make my argument more explicitly:

The only difference between the two scenarios for late value foo is that in one scenario, some code has to explicitly assign foo = something, whereas in the other, = something is assigned implicitly when needed. This difference is reflected in the existence or nonexistence of an initializer in the declaration.

Otherwise, there is no difference in semantics AFAICT.

ghost commented 7 years ago

it seems to match the English meaning of "late"

Just because it makes sense in English doesn’t mean it makes sense in Ceylon.


the actual code generation is pretty similar: instead of throwing an exception, we initialize it to the given expression

~

Otherwise, there is no difference in semantics AFAICT.

What do you mean by there is no difference? late, in its former meaning, is used to tell the typechecker to shut up about the initialization of the value, allowing you to initialize (or to not initialize) the value whenever you want. This is completely different from what memoized would do.

I guess you are trying to say that late without the assignment would implicitly be equivalent of this pseudo‐Ceylon:

late String foo = assert(false);

But that’s not correct. As you guys have been discussing, late allows you to assign things to it whenever you want, memoized would not. That’s a very different change in semantics in my opinion. I honestly cannot see what you think is similar about these.


it seems to me that issue #6721 applies equally to lazy fields as to late fields (they can both be placed in the declaration section)

Yeah, the can both appear in the declaration section, sure, but for completely different reasons. memoized values can because they are lazy. late values can because they are not assigned yet, and don’t run any code. Supposedly, non‐late values that are assigned later also can appear in the declaration section:

class Foo()
{
    shared void foo() => ...;
    String bar;
    shared void baz() => ...;
    // end of declaration section
    bar = "hello";
}
ghost commented 7 years ago

Sorry, I forgot to at-mention you, @gavinking.