language icon indicating copy to clipboard operation
language copied to clipboard

[Feature Request] Add "placeholders"

Open Myzel394 opened this issue 1 year ago • 7 comments

Use case

I found out that I'm often creating a variable for clarification instead of writing it in one line.

Example (pseudo code):

Optimal code for humans

Location location = await Getter.getLocation();
Speed speed = await Getter.getSpeed();

Resul result = await Result(
    location: location.
    speed: speed,
);

Optimal code for efficiency

Result result = await Result(
    location: await Getter.getLocation(),
    speed: await Getter.getSpeed(),
);

While the given example is admittedly quite readable, there are often cases where it's easier to create a variable for clarification.

Proposal

It would be quite nice if you could use named "placeholders" that replace code parts during compilation.

Example:

location: await Getter.getLocation();
speed: await Getter.getSpeed();

Resul result = await Result(
    location: location.
    speed: speed,
);

After compilation this code will result in:

Result result = await Result(
    location: await Getter.getLocation(),
    speed: await Getter.getSpeed(),
);

Advantages to the current Flutter version

You don't have to create variables just for better DX (Developer Experience). This will primarily lead to less memory usage, which means there will be more memory available, which means faster, better and more efficient apps! 😆

Myzel394 avatar Jul 13 '22 08:07 Myzel394

Sounds like you're looking for the readability of variables but without the technical overhead of variables. Someone correct me if I'm wrong, but I assumed that using final would allow the compiler to optimize the variable away if possible.

Levi-Lesches avatar Jul 13 '22 09:07 Levi-Lesches

I don't actually see why the "optimal code for efficiency" is any more efficient than the "optimal code for humans". The output from a moderately competent compiler should be indistinguishable. (I hope our compilers are moderately competent, but with async, I'm not promising anything.)

What you are describing looks remarkably like late final variables - except that late variable initializers can't contain await, so you can't use those here. They can't contain await for a good reason, so the asynchronous placeholders allowing it is itself worrisome. It would be asynchronous expressions in a seemingly non-asynchronous piece of code. The Result(location: location, speed: speed) doesn't look like it contains two awaits. Users won't necessarily be able to figure out which order things happen in. So, the "for humans" code might not actually be good for human readability, if figuring out evaluation order is important.

lrhn avatar Jul 13 '22 13:07 lrhn

@lrhn Yes it's like when you use final with a good compiler. Although I don't know if a compiler will also efficiently optimize cases when you use the variable multiple times.

Example:

final Location location = await Getter.getLocation();
final Speed speed = await Getter.getSpeed();

final Result result = await Result(
    location: location.
    speed: speed,
);
final AnotherResult anotherResult = await AnotherResult(
    location: location
);

Will this force the compiler to create an extra variable for location and efficiently place speed into the Result construction?

Myzel394 avatar Jul 13 '22 17:07 Myzel394

Evaluation order matters, so the compiler will definitely evaluate location before speed and both before calling Result. In compilation, evaluation order is pretty much the only thing which matters, which is why

final Location location = await Getter.getLocation();
final Speed speed = await Getter.getSpeed();

final Result result = await Result(
    location: location.
    speed: speed,
);

and

final Result result = await Result(
    location: await Getter.getLocation().
    speed: await Getter.getSpeed(),
);

should be indistinguishable. It's not that the compiler puts await Getter.getSpeed() into the Result constructor call, instead it throws away all the source code and generates some native code with the same effect. Since the two pieces of code has exactly the same effect, there result can be the same. Variables do not exist at runtime, it's a source concept that allows the source code to refer to the same thing from different places. How those variables are implemented is entirely up to the compiler.

Adding

final AnotherResult anotherResult = await AnotherResult(
    location: location
);

after the first call just means that the value stored in the location variable needs to be alive at the other call too. The compiler can store it on the stack or in a persistent register, the only thing it can't do is throw away the value before it has been used again. It doesn't introduce an extra variable, it extends the lifespan of the value computed by await Getter.getLocation() to extend past the call to AnotherResult, and the lets the compiler backend find a way to keep that value around. (So it requires at least one register or memory location more than the earlier example to get the value past the await Result(..) computation.)

lrhn avatar Jul 13 '22 17:07 lrhn

@lrhn thanks for the detailed answer! :heart:

So it requires at least one register or memory location more than the earlier example to get the value past the await Result(..) computation.

Here's where my proposal of placeholders comes in! Instead of using more memory, placeholders will simply recalculate the value.

This is not applicable in every scenario, but there are certainly a lot of times where it'd have been better if an app simply had recalculated a value instead of eating up memory.

Myzel394 avatar Jul 13 '22 18:07 Myzel394

Recalculating the value, which can possibly be an asynchronous computation, sounds very error-prone.

If you want to just share the code, there is always functions:

Location location() async => await Getter.getLocation();

final Result result = await Result(
    location: await location().
    speed: await Getter.getSpeed(),
);
final AnotherResult anotherResult = await AnotherResult(
    location: await location()
);

The big issue here is that you need to write await again, because we can't abstract over asynchrony. And it requires the compiler to efficiently inline the function for it to be as efficient (and be smart about the double await).

So, really, what a placeholder is, is more like a local getter:

Location get location => await Getter.getLocation();

but one which can only be used internally in the same function, is guaranteed to be inlined, and can therefore use the same asynchrony as the surrounding function.

Seems too complicated for what it brings to the table :)

lrhn avatar Jul 13 '22 18:07 lrhn

Instead of using more memory, placeholders will simply recalculate the value.

This is also not a universally accepted tradeoff. For example, if you have a function that computes a number, or a function that makes a network call to get an object, these are values most people would want to be cached (ie, trading off memory for time). If you want to pass around the instructions to compute the value instead of the value itself, use a function as in @lrhn's example because that's what functions are for.

Levi-Lesches avatar Jul 13 '22 22:07 Levi-Lesches