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

Feature request: lazy Val/Var #57

Open
Grannath opened this issue Jun 17, 2016 · 7 comments
Open

Feature request: lazy Val/Var #57

Grannath opened this issue Jun 17, 2016 · 7 comments

Comments

@Grannath
Copy link

Hi,

I'm thinking about an extension to Val/Var, or rather to the provided factory methods. The idea is to provide a Supplier that gets called the first time the value is actually read.

Val<String> lazyVal = Val.lazy(this::calculateComplicatedName); // not called yet
// do some stuff
System.out.println(lazyVal.getValue()); // calculateComplicatedName called now
System.out.println(lazyVal.getValue()); // result was cached, not called again

This is obviously useful for cases where a default value is expensive to calculate. But I was actually thinking about loading remote values on demand. Instead of loading everythink immediately, this would provide a clean API for lazy loading. For remote calls, this would have to be done async.

Val<String> lazyVal = Val.lazyAsync(String placeholder, this::loadNameFromServer); // not called yet
// do some stuff
System.out.println(lazyVal.getValue()); // returns placeholder, loadNameFromServer started in separate thread
// some time later
System.out.println(lazyVal.getValue()); // loading was finished, returns result now

While it's easy enough for a Val, because it cannot change, I'm not so sure how or even if it's useful for a Var. But I would be grateful for any input on this. If there is interest, I would be happy to code it myself and start a pull request.

@TomasMikula
Copy link
Owner

TomasMikula commented Jun 17, 2016

Hi,

if you just need a lazy value, you don't need a Val interface for that. Supplier is sufficient. You can wrap a Supplier that always evaluates the value in one that memoizes the value after first evaluation.

If you want to wrap a Supplier (lazy or not) in a Val, you can already do that by Val.create(supplier). Note that such Val will never be invalidated.

@Grannath
Copy link
Author

Grannath commented Jun 20, 2016

Hi,

thanks for the hint. I just checked your code. I did not realize Val was already lazy. I'm pretty sure that the "vanilla" classes from JavaFX always evaluate those expressions right away. I guess that's where I got the impression.

I still want to make something clear. I think it's what you wanted to tell me, but for anyone else checking in I want to be a bit more explicit.

If you want to wrap a Supplier (lazy or not) in a Val, you can already do that by Val.create(supplier). Note that such Val will never be invalidated.

While technically true, it will still potentially reevaluate the Supplier quite often.

if(!valid || !isObservingInputs()) {
    value = computeValue();
    valid = true;
}

As long as no observer is registered, the Supplier is called every time. That's where the wrapping Supplier comes into play to store the result. So far, this is not really worth a pull request. That's where we agree.

But I'm mostly interested in the second, async version. In my project, we need to load a lot of rather large objects and their dependencies from a remote server, over a bad VPN connection. I want to utilize the whole focus on Observable in JavaFX for efficient lazy loading. Only get the stuff from the server when it's actually being shown. Having a Val trigger the loading on either getValue() or addChangeListener(...) or whatever and keeping track of the initialization state is really useful.

So I will probably write both methods anyway. Question is, is there any interest in this out there? It's your framework, so ultimately your choice. Consider it a free offer. :)

EDIT: I've just found a nice use case for a lazy Val. In multiple cases in my project, the value for a constant Val requires dependencies injected by a DI framework. Instead of creating the Val in a @PostConstruct method, which requires the field to be non-final. I can create it at the declaration without running into problems with the initialization order.

@TomasMikula
Copy link
Owner

TomasMikula commented Jun 20, 2016

If you want to wrap a Supplier (lazy or not) in a Val, you can already do that by Val.create(supplier). Note that such Val will never be invalidated.

While technically true, it will still potentially reevaluate the Supplier quite often.

Sorry, I meant "memoizing or not." But you probably figured that out.

Having a Val trigger the loading on either getValue() or addChangeListener(...) or whatever and keeping track of the initialization state is really useful.

If you want the loading to be asynchronous, what would you return from getValue(), when you don't have the value yet?

I've just found a nice use case for a lazy Val. In multiple cases in my project, the value for a constant Val requires dependencies injected by a DI framework. Instead of creating the Val in a @PostConstruct method, which requires the field to be non-final. I can create it at the declaration without running into problems with the initialization order.

I don't quite understand. You still need to inject the value somewhere, so something else will have to be non-final instead, right?

@Grannath
Copy link
Author

Grannath commented Jun 21, 2016

If you want the loading to be asynchronous, what would you return from getValue(), when you don't have the value yet?

True, you need to supply a placeholder. For a String, for example, something like "loading...".

I don't quite understand. You still need to inject the value somewhere, so something else will have to be non-final instead, right?

Not necessarily. Consider this:

@Inject
private SomeService service;
private final Val<String> serviceResult = Val.constant(service.calc());

Depending on the framework, service might even be final. That depends entirely on how the injection is done. But it will crash anyway, because when the Val is created, the service is not yet injected. But:

@Inject
private SomeService service;
private final Val<String> serviceResult = Val.lazy(() -> service.calc());

This will work. Only when getValue() is called will the service be used, which is (or should be) after the complete initialization including injection of dependencies. Which is, by the way, exactly what I implemented yesterday for exactly this reason. ;)

PS: I didn't say it yet, but I will say it now: Thanks for a great farmework. Not just the API and features, also the codebase. It's really well structured, even more so when compared to the mess that is JavaFX.

@TomasMikula
Copy link
Owner

private final Val<String> serviceResult = Val.lazy(() -> service.calc());

That still can be done by

Val.create(memoize(() -> service.calc()));

where

static <T> Supplier<T> memoize(Supplier<T> supplier);

So the convenience for the user of your proposed method is not having to implement that memoize method. Alright, I'm on board. What about the async version? Should it take a CompletionStage and an Executor? What to do on error?

@Grannath
Copy link
Author

For error handling, I would give the same arguments you had when it came to errors during event stream processing. Either you handle it directly inside the Supplier (or whatever interface), potentially return some predefined error value such as "Loading failed!" for Val<String>. Alternatively you install an exception handler and let it bubble up. The Val cannot handle an error, so these are the only practical options.

I will prepare a pull request, so that we can discuss the async version a bit easier. I'll implement it after we agreed on the API.

@TomasMikula
Copy link
Owner

Exceptions that are programming bugs should bubble up. But asynchronous task are often associated with inherently unsafe operations like (network) I/O. Those should be reflected in the return type. So perhaps let the async version(s) return Val<Try<T>>.

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

No branches or pull requests

2 participants