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

Attribute lazy initialization #3544

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

Comments

Projects
None yet
@CeylonMigrationBot

CeylonMigrationBot commented Oct 18, 2012

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

This comment has been minimized.

CeylonMigrationBot commented Oct 18, 2012

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

This comment has been minimized.

CeylonMigrationBot commented Oct 18, 2012

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

This comment has been minimized.

CeylonMigrationBot commented Oct 19, 2012

[@luolong]
+1 for this syntax:

Foo foo ?= createFoo();
@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

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

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

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

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

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

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

[@quintesse] once ?

@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

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

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

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

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

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

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

[@FroMage] Sure.

@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Jan 3, 2013

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

This comment has been minimized.

CeylonMigrationBot commented Jan 5, 2013

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

This comment has been minimized.

CeylonMigrationBot commented Jan 5, 2013

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

@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Apr 10, 2015

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

This comment has been minimized.

CeylonMigrationBot commented Oct 30, 2015

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

@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Oct 30, 2015

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

This comment has been minimized.

CeylonMigrationBot commented Nov 2, 2015

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

This comment has been minimized.

CeylonMigrationBot commented Nov 2, 2015

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

This comment has been minimized.

CeylonMigrationBot commented Nov 2, 2015

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

@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Nov 2, 2015

[@quintesse] +1 on once

@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Nov 2, 2015

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

This comment has been minimized.

CeylonMigrationBot commented Nov 3, 2015

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

This comment has been minimized.

CeylonMigrationBot commented Nov 4, 2015

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

@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Nov 4, 2015

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

shared memoized Integer val => computeVal();
@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Nov 6, 2015

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

This comment has been minimized.

CeylonMigrationBot commented Nov 7, 2015

[@gdejohn] 👍 for memoized.

@CeylonMigrationBot

This comment has been minimized.

CeylonMigrationBot commented Nov 7, 2015

[@lukedegruchy] I vote for cached.

@quintesse

This comment has been minimized.

Contributor

quintesse commented Jan 23, 2017

If I understand this correctly

Isn't it more that whatever comes first happens? Meaning that if you read first the specifier gets used while if you write first that value gets used instead of the specifier?

@Zambonifofex

This comment has been minimized.

Contributor

Zambonifofex 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

This comment has been minimized.

Contributor

jvasileff commented Jan 23, 2017

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

@lucaswerkmeister

This comment has been minimized.

Member

lucaswerkmeister commented Jan 23, 2017

@quintesse I would also strongly object to that interpretation.

@quintesse

This comment has been minimized.

Contributor

quintesse commented Jan 23, 2017

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

@gavinking

This comment has been minimized.

Contributor

gavinking commented Jan 23, 2017

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

This comment has been minimized.

Contributor

gavinking commented Jan 23, 2017

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

@tombentley

This comment has been minimized.

Contributor

tombentley commented Jan 23, 2017

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

This comment has been minimized.

Member

lucaswerkmeister commented Jan 23, 2017

@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

This comment has been minimized.

Contributor

gavinking commented Jan 23, 2017

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.

@Zambonifofex

This comment has been minimized.

Contributor

Zambonifofex 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

This comment has been minimized.

Contributor

gavinking commented Jan 23, 2017

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.

@Zambonifofex

This comment has been minimized.

Contributor

Zambonifofex 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

This comment has been minimized.

Contributor

gavinking commented Jan 23, 2017

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

This comment has been minimized.

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

This comment has been minimized.

Contributor

gavinking commented Jan 23, 2017

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.

@Zambonifofex

This comment has been minimized.

Contributor

Zambonifofex 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

This comment has been minimized.

Contributor

tombentley commented Jan 24, 2017

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

@gavinking

This comment has been minimized.

Contributor

gavinking commented Jan 24, 2017

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 join this conversation on GitHub. Already have an account? Sign in to comment