Skip to content
This repository has been archived by the owner on Apr 13, 2023. It is now read-only.

Attribute lazy initialization #3544

Closed
CeylonMigrationBot opened this issue Oct 18, 2012 · 94 comments
Closed

Attribute lazy initialization #3544

CeylonMigrationBot opened this issue Oct 18, 2012 · 94 comments

Comments

@CeylonMigrationBot
Copy link

[@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
Copy link
Author

[@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
Copy link
Author

[@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
Copy link
Author

[@luolong]
+1 for this syntax:

Foo foo ?= createFoo();

@CeylonMigrationBot
Copy link
Author

[@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
Copy link
Author

[@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
Copy link
Author

[@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
Copy link
Author

[@quintesse] once ?

@CeylonMigrationBot
Copy link
Author

[@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
Copy link
Author

[@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
Copy link
Author

[@gavinking]

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

Only when:

  • the RHS type is narrower than the LHS type, and
  • the assignment appears as a subexpression (rather than as a statement).

@CeylonMigrationBot
Copy link
Author

[@FroMage] Sure.

@CeylonMigrationBot
Copy link
Author

[@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
Copy link
Author

[@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
Copy link
Author

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

@CeylonMigrationBot
Copy link
Author

[@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
Copy link
Author

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

@CeylonMigrationBot
Copy link
Author

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

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


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

@CeylonMigrationBot
Copy link
Author

[@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
Copy link
Author

[@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 [email protected]:

@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
Copy link
Author

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

@CeylonMigrationBot
Copy link
Author

[@quintesse] +1 on once

@CeylonMigrationBot
Copy link
Author

[@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
Copy link
Author

[@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
Copy link
Author

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

@CeylonMigrationBot
Copy link
Author

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

shared memoized Integer val => computeVal();

@CeylonMigrationBot
Copy link
Author

[@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
Copy link
Author

[@gdejohn] 👍 for memoized.

@CeylonMigrationBot
Copy link
Author

[@lukedegruchy] I vote for cached.

@ghost
Copy link

ghost commented Jan 23, 2017

By the way, I’m not saying that I agree with @gavinking here, I agree with @lucaswerkmeister in that this feels counterintuitive, and that I’d rather have that kind of code rejected by the type‐checker.

That’s exactly why I think there should be a different annotation: because the semantics are very different from each other.

@jvasileff
Copy link
Contributor

Has it been determined if shared default late + specified will be allowed, and if allowed, the semantics?

@lucaswerkmeister
Copy link
Contributor

@quintesse I would also strongly object to that interpretation.

@quintesse
Copy link
Contributor

@lucaswerkmeister not saying I don't agree with you, but a good reason as to why you strongly object would be nice :)

@gavinking
Copy link
Contributor

What @quintesse said is the interpretation I favor. That way the specifier defines a default value that is used if you never assign it anywhere else. Sorta but not exactly like defaulted parameters.

@gavinking
Copy link
Contributor

And the reason I favor this interpretation is that it gives 'late' a more consistent behavior between the two cases.

@tombentley
Copy link

Which is fine, and I appreciate that. But @lucaswerkmeister has a good practical point which is that very often people want to lazily initialize a value without allowing the possibility for a client to overrule that value. So maybe it is a mistake to try to squeeze those semantics into late.

@lucaswerkmeister
Copy link
Contributor

@quintesse well, I already said it above – I see this feature as a way to defer initialization while still remaining in control of it (my class doesn’t rely on someone else calling an init method anymore), whereas with that interpretation, people can completely sidestep the initialization I had in mind.

@gavinking
Copy link
Contributor

So maybe it is a mistake to try to squeeze those semantics into late.

Perhaps, but frankly I don't find it very appealing to introduce a new annotation that is in so many other respects so extremely semantically similar to late.

@ghost
Copy link

ghost commented Jan 23, 2017

@gavinking

I honestly can’t see how it’s extremely semantically similar to late. I can see a faint connection, but I honestly don’t understand what you see so similar about those two behaviors. They do completely different things, are implemented completely differently, and would be specced differently aswell.

I don’t think trying to merge those two behaviors into one single annotation would be a good idea. If you want the behavior you are describing, you’d annotate the value as late memoized.

@gavinking
Copy link
Contributor

They're very similar in how they affect definite assignment checking and determination of where the initializer section ends. This is, of course, the most interesting thing about these annotations.

They do completely different things, are implemented completely differently, and would be specced differently aswell.

That's just not true at all.

@ghost
Copy link

ghost commented Jan 23, 2017

@gavinking

[…] determination of where the initializer section ends

Sure.

[…] affect definite assignment checking

I don’t understand. By @lucaswerkmeister’s definition, memoized values would always be considered as assigned, while current late values would always be considered as assignable.

That's just not true at all.

I can’t speak for the implementation, but I definitely feel like they’d do completely different things. And if you could exemplify how you think the spec would be, and how the two behaviors would overlap, it’d great (I’m trying to convince myself that they are similar and failing; if you could do that, that’s what would be great).

@gavinking
Copy link
Contributor

By @lucaswerkmeister’s definition,

That is not the definition that I have implemented!

while current late values would always be considered as assignable.

No, that's just not the current semantics! late values can be assigned exactly once. This new feature just tells you what to do if the late value is accessed before it is assigned, i.e. instead of an exception, evaluate the initializer. That's an incredibly simple, localized change.

I can’t speak for the implementation

Here's how I implemented the typechecker part of this: @eb5b3b8ed5f6c36a7642e2511e1b63b7e09ab68f. Note that I deleted more code than I added. It made the typechecker simpler.

I definitely feel like they’d do completely different things.

I don't care about vague feelings. I care about objectively measurable things.

And if you could exemplify how you think the spec would be

The spec change is trivial as hell:

  1. remove the restriction that a late value can't have an initializer (arguably more regular).
  2. specify, that in the case of access to an uninitialized late value, if there is an initializer, use that to initialize the value instead of throwing an exception.

That's it, AFAICS.

@gavinking
Copy link
Contributor

gavinking commented Jan 23, 2017

@lucaswerkmeister

with that interpretation, people can completely sidestep the initialization I had in mind.

I understand this concern, and I'm sympathetic to it, but OTOH, it's quite trivial to get the behavior you're looking for:

late value _thing = Thing();
shared Thing thing => _thing;

This way, you avoid sharing the "assignability" of _thing.

Furthermore, if you happen to be implementing an API interface, you don't even need to do that.

@gavinking
Copy link
Contributor

Note that what attracted me to this whole solution was how tiny of a change it was to the Ceylon language. I got to piggyback/reuse the existing behavior of something that already existed for a long time.

@ghost
Copy link

ghost commented Jan 23, 2017

@gavinking

That is not the definition that I have implemented!

Well, I agree that, by your definition, it’s really similar to late today.

while current late values would always be considered as assignable.

late values can be assigned exactly once.

I’m saying that they are considered as assignable by the compiler. I know trying to assign them more than once would result in an error at run‐time.

This new feature just tells you what to do if the late value is accessed before it is assigned

The spec change is trivial as hell:

Under your definition. Under @lucaswerkmeister’s definition (the one I strongly prefer), this simply wouldn’t be the case.

I think your definition would simply not be that useful. When will someone actually prefer to allow their interface user to actually override their given memoized value? When will that actually be better than @lucaswerkmeister’s definition?

@tombentley
Copy link

@gavinking fixed. If we want to change the semantics of late+specifier, that's another issue.

@gavinking
Copy link
Contributor

fixed.

Thanks Tom.

If we want to change the semantics of late+specifier, that's another issue.

Yeah, let me reflect on this. If we do, it's just a typechecker change.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests