Replies: 18 comments
-
In some ways I think this is the wrong way around. Rather than specifying that the expression is lazy, I think specifying that the type is lazy would be a better approach. It would allow the compiler to infer that the expression being assigned is lazy. It would also allow for seamless lazy parameters for functions, which I think would be a really big win: lazy string data1 = "123"; Becomes: var data1 = new Lazy<string>(() => "123"); When I saw the title of this proposal I thought "Yes, someone's written up a feature I've been desperate for". But this isn't quite it. In my C# functional library, I simulate pattern matching on types like Option<int> option = Some(123);
Option<int> none = None;
var result = option.Match(
Some: x => x * 2,
None: () => 0
); For convenience there are 'default handling methods': var result = option.IfNone(() => 0); Really what I want is this: var result = option.IfNone(0); Where the public A IfNone(lazy A result) =>
IsNone
? result
: Value; Expanded it would look like so: public A IfNone(Lazy<A> result) =>
IsNone
? result.Value
: Value; This could also work: public lazy A IfNone(lazy A result) =>
IsNone
? result
: Value; Expands to: public Lazy<A> IfNone(Lazy<A> result) =>
new Lazy<A>(() => IsNone
? result.Value
: Value); So the laziness can propagate. It seems a much simpler proposition to have lazy as a modifier for a type, because essentially it's just wrapping with |
Beta Was this translation helpful? Give feedback.
-
I'd absolutely hate for the compiler to bless one particular mechanism, like |
Beta Was this translation helpful? Give feedback.
-
@jnm2 Why? It's exactly the same as the compiler deciding that all expressions must be evaluated strictly. A lazy value has either been evaluated or not, and will be evaluated at the point of access. When it's been evaluated it just acts like a regular strict value, and will be collected when the running application drops the reference to the It's exactly how laziness works in Haskell, behind the scenes there are thunks that are evaluated on use. Haskell has the problem that everything is lazy by default, and you opt into strictness. I think this approach of everything being strict by default but we opt into laziness is very powerful. It is especially useful for writing functional code in C#, where the same term may be used several times in an expression. |
Beta Was this translation helpful? Give feedback.
-
@louthy your idea is not conflicted with my proposal. The lazy field and property designs just have the identify syntax and effect compared with your idea (although we are thinking from different origins). The only missing part is for lazy parameters, and I've added it to my original proposal. You may take a double check to consider if my explaination meets your requirement. Thank you! |
Beta Was this translation helpful? Give feedback.
-
Why? Because It is not exactly the same as something that happens at compile-time. I'm talking about what happens at runtime.
Because if you mandate that it use
|
Beta Was this translation helpful? Give feedback.
-
It's not comparing and swapping, it's lazy evaluation of an expression. In terms of hidden allocations, we already have those with lambdas, transparent types in LINQ, etc. Laziness will necessarily come with a cost because of its need to wrap an expression, that's unavoidable.
I think you're confusing laziness with conditional assignment. It's the not same thing. This is purely moving the evaluation to the point where the variable is used for the first time, rather than when it's assigned.
Assuming by
What would it lock on? Where would the 'i have a value' state be stored? Sounds like allocation costs to me.
Compare with what?
Not at all. Any expression where the evaluation is delayed must be captured in a 'thunk'.
There is only one strategy to an in-built system like this. All lazy languages work in the same way. You assign an expression to a variable, and it's evaluated on usage. The technicalities of implementing that efficiently should obviously be discussed, but I don't see any other strategy for the timing of the evaluation that makes sense. And I find it hard to see how an expression can be captured without a thunk/capture object of some sort. |
Beta Was this translation helpful? Give feedback.
-
The only thing I don't agree with is the keyword var data = lazy GetMyName(); I prefer this: lazy var data = GetMyName(); Mainly because the expression is not the lazy thing here, it's the assignment to a variable that is delayed. For example. what happens here?: var data = (lazy GetFirstName()) + GetLastName(); Is Or, var data = (lazy GetFirstName()) + (lazy GetLastName()); Is Let's take a look at some of your other examples: string data1 = lazy "123";
lazy string data1 = "123"; Which expands to: Lazy<string> data1 = new Lazy<string>(() => "123); On to the next one, this would be lazy, but I don't see the need for the Lazy<string> data2 = lazy "123"; I think should be: Lazy<string> data2 = "123"; Which expands to: Lazy<string> data2 = new Lazy<string>(() => "123); With your last example:
I think a
Which expands to: Lazy<string> data3 = new Lazy<string>(() => "123); Ultimately I don't think expressions should be annotated with btw, 'Clourses', should be 'Closures' :) |
Beta Was this translation helpful? Give feedback.
-
@louthy The lazy implementation will use either a lock or compare and swap, which is what I was referring to. If you are multithreading there are times when you already have a lock, and so may as well reuse it. If you are not multithreading, all this is overhead. I may have lost us a bit by using The point I'm trying to make is that |
Beta Was this translation helpful? Give feedback.
-
You're still missing the point: that the evaluation isn't triggered at the point of assignment. It's triggered by access to the lazy variable. That lazy variable has to contain details of the expression to evaluate. It isn't about the current state of the variable (for comparing) other than it's 'not evaluated'. So let's say you have a function that returns a lazy value: lazy int LazyFunc()
{
int y = 10;
lazy var x = StrictDoSomeWork() + y; // Creates a lazy int
return x; // returns the lazy int
}
int StrictDoSomeWork() => 10;
int StrictFunc(bool eval, lazy int x)
{
return eval
? x // Lazy int assigned to strict return type of int which forces evaluation
: 0;
}
var r1 = StrictFunc(true, LazyFunc()); // 20
var r2 = StrictFunc(false, LazyFunc()); // 0 (and StrictDoSomeWork has not run) In your world of compare and swap, what is
No it doesn't. The scenario I originally mentioned is not served by using The reasons I suggested are to do with a generalised system for propagating laziness so that code that doesn't need to be run isn't (like in ternary expressions, if/then/else, logical OR propagation, and null propagation) - I am looking for a general solution to that issue - this is especially useful for 'expression oriented' / functional programming; @sgjsakura's suggestion is more about the classic lazy loading pattern, but it just so happens that the solution I suggested will work for both. I cannot see how your suggestions will support a feature like this. Here is what it would look like with Lazy<int> LazyFunc()
{
int y = 10;
Lazy<int> x = new Lazy<int>(() =>StrictDoSomeWork() + y);
return x;
}
int StrictDoSomeWork() => 10;
int StrictFunc(bool eval, Lazy<int> x)
{
return eval
? x.Value
: 0;
}
var r1 = StrictFunc(true, LazyFunc()); // 20
var r2 = StrictFunc(false, LazyFunc()); // 0 (and StrictDoSomeWork has not run) There's a simplicity to it which I find very appealing. Yes it has a cost, but in the big scheme of things the cost is tiny, and also, this is a managed runtime, if you're concerned about memory allocation then you're in the wrong runtime. Most people have learned to trade off certain costs to write better code that's easier to maintain. I am happy I don't spend all my time trying to hand optimise 3D graphics engines to fit in the 4K instruction cache of the PS1 any more! And still, if you want the semantics of If you believe it's possible to do this without any cost, please show your working. How would you achieve the code above with compare and swap? |
Beta Was this translation helpful? Give feedback.
-
@louthy Thank you for your feedback, however I think the var a = lazy 1;
lazy var b = 1;
var c = a + 1;
var d = b + 1; There are different implementations for this design, and I will show them as following: Without Lazy Type GenerationThe lazy expression and decleration both generates a lazy reference and any reference of lazy expression will access its value, no int a = new Lazy<int>(()= > 1).Value;
// The original code is a shortcut as var b = lazy 1; so the result will be:
int b = new Lazy<int>(()= > 1).Value;
int c = a + 1;
int d = b + 1; With Lazy Type GenerationThe lazy expression generates a lazy reference, a non-lazy left-value reference or any right-value reference will access its value, and a lazy left-value reference will reference the lazy object itself, genreate a so the final generated code will be: int a = new Lazy<int>(()= > 1).Value;
Lazy<int> b = new Lazy<int>(()= > 1);
int c = a + 1;
int d = b.Value + 1; The differences between them is how to handle lazy declerations and futher usages, You can see in the 2nd manner, 2 same statement generate different codes, the compiler should do more work to correctly handle the lazy variable references. Another problem may causes if the lazy modifier is applied on methods or fields, since they need an explicit CLR level signature and change may cause breaking changes, for example: lazy int M() => 1;
var data = M(); In the first manner, the generated code will be: int M() => new Lazy<int>(()=>1).Value;
var data = M(); In the seconed manner the generated code will be: Lazy<int> M() => new Lazy(()=>1);
var data = M().Value; The problem is if your add the If you want to keep lazy value non-evualuated until the first access, you may write the following code as: lazy int F() => 1;
lazy int G() => F(); and it will be compiled as: int F() => new Lazy<int>(() => 1).Value;
int G() => new Lazy<int>(() => F()).Value; The lazy chain will keep un-evaluated untill a final call is made from one of them. This may be lengthy, however right now I have not found out a solution for both keeping CLR compatibility and transfer lazy references, if you have better idea, it will be my pleasure to update your design to this issue. Thank you :-) |
Beta Was this translation helpful? Give feedback.
-
I think the word lazy is misleading here. It should be called
Compiler generated code,
Many times, we don't need thread safety just to store variable that probably was retrieved from DI container. With locking...
Generated code,
This pattern is really handy in improving performance. |
Beta Was this translation helpful? Give feedback.
-
I, for one, would really really like this feature, even if it was limited to properties (since that's where I use Lazy 99% of the time). They are also called "cached properties" by other people who have proposed similar ideas in the past (perhaps in the Roslyn repo?). Basically, if this: public class Foo
{
public ExpensiveThing Thing { get; } = new ExpensiveThing();
} is just shorthand for the following: public class Foo
{
private ExpensiveThing _thing = new ExpensiveThing();
public ExpensiveThing Thing
{
get
{
return _thing;
}
}
} then I'd like: public class Foo
{
public lazy ExpensiveThing Thing { get; } = new ExpensiveThing();
} to be shorthand for this: public class Foo
{
private ExpensiveThing _thing;
private bool _thingInitialised;
public string Thing
{
get
{
if(!_thingInitialised)
{
_thing = new ExpensiveThing();
_thingInitialised = true;
}
return _thing;
}
}
} |
Beta Was this translation helpful? Give feedback.
-
I suggest to put public class Foo
{
public ExpensiveThing _lazyField = defer new ExpensiveThing();
public ExpensiveThing Thingi {
get {
if (_lazyField.obj == null) return null;
[...] return _lazyField;
}
}
public ExpensiveThing Thing { get; } = defer CreateExpensiveThing();
} Of course this must emmit guard code to every flow occurance of the (backing) field. So, |
Beta Was this translation helpful? Give feedback.
-
You don't need all that, what you need is just an implicit conversion operator: class MyLazy<T>
{
Lazy<T> lazy;
public MyLazy(Func<T> f) => lazy = new Lazy<T>(f);
public static implicit operator T(MyLazy<T> myLazy) => myLazy.lazy.Value;
}
…
MyLazy<string> myStr = new MyLazy<string>(() => "");
Debug.Assert("" == myStr); // indirectly calls Value You could try proposing adding such implicit operator to |
Beta Was this translation helpful? Give feedback.
-
@louthy The back and forth was too tedious for me, so I'll let Matt Warren explain it much better. #681 (comment) |
Beta Was this translation helpful? Give feedback.
-
I prefer It could be transpiled as this public lazy string Text => GetTextFromSomeWhere(); // Can only be get only property
//
Console.WriteLine(Text);
Console.WriteLine(Text.Length); Would be transpiled to public Lazy<string> Text = new Lazy<string>(() => GetTextFromSomeWhere());
// every places using this will append .Value at compile time
Console.WriteLine(Text.Value);
Console.WriteLine(Text.Value.Length); The point here is it can seamlessly use any object and internal value of that class directly public lazy SomeObject obj => GetTextFromSomeWhere();
//
Console.WriteLine(obj.SomeProperty);
obj.DoSomething(); So @svick suggestion not solve this Alternatively I think we could just shorthand public Lazy<string> Text => GetTextFromSomeWhere();
//
Console.WriteLine(Text?.Length); // can access member directly
Console.WriteLine(Text?); // should let it support empty ? operator ??? Would be transpiled to public Lazy<string> Text => GetTextFromSomeWhere();
//
Console.WriteLine(Text?.Value.Length); // can access member directly
Console.WriteLine(Text?.Value); |
Beta Was this translation helpful? Give feedback.
-
Almost a year later, any updates on this? |
Beta Was this translation helpful? Give feedback.
-
@weitzhandler Nobody from the language design team has championed this proposal so there are no updates. Someone from the community could fork Roslyn and try to prototype it out. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Since the original issue dotnet/roslyn#3251 is closed, I'm transfering it to here, please close it if it's duplicated with any other issue. Also, it seems can be done (but I'm not sure) if #107 is fully implemented.
Background
In practice, "Evaluate while First Access" is a common requirement in library and application developement, especially in property design. A common simple design model for this requirement may like:
However this method is not perfect because:
null
is not safe. (The method may also return null or any other perdefined value).The
System.Threading.LazyInitializer
helper class give and easier way to create lazy evaluated values with thread-safe lock, however, it still requires declaring and state flag, sync lock and background field.Proposal
All of the above items may be implemented by compiler, which can greatly reduce the workload
of developers. For implementation, language may introduce a new "lazy" keyword, which can be applid on any field, property, or even in-line expressions.
Lazy Expressions
for example:
the "lazy" keyword indicate the followed expression should be computed only once using an EnsureInitialized call with one compiler generated state flag and compiler generated backfield value. The generated code may like:
And then the evaluation will be only once, and the subsequent calls of this code snippet will not evaluate the expression again.
The lazy expression can be valid language expressions, and the whole expression will always be wraped with an anomynous (or compiler generated) method. e.g. the expression
lazy (M1() + M2())
is always acceptable.Lazy Properties, Fields, and Variables
The lazy keyword may also be applied on an property, field, or variable with inplace initializers, it is a shortcut syntax sugar to simplify the lazy expressions when the right side is complex. For example, a lazy property defined as
public lazy string MyName => a + b;
is equivelent topublic string MyName => lazy (a + b);
.Lazy Parameters
Thanks to @louthy for this idea.
The
lazy
keywords may also be applied on a parameter as a syntax sugar, which means all argument is automatically be lazy expression. e.g. If a method is with the following signature:It means the invocation
M(1)
is automatically expanded intoM(lazy 1)
.Problems
Clourses
The most important problem for lazy expressions may be clourse due to introducing local variables. This problems is just the same as LINQ and should be carefully designed. Limited the keyword application for only on properties and fields may mitigating this problem since they do not generate clourses.
Code Generation Interactions
Every new code generation technique or syntax sugar will add complexity to the compiler, and the problem will be even more tricky when different techinques are applied in one same scene. The lazy expression be combined with iteration pattern (
yield
keyword), async-machine pattern (async
keyword), and so on. Unforeseen issues may occur during the language design process.Lazy Implementation
Beside the
LazyInitializer
,System.Lazy<T>
is also an alternative lazy evalutation mode. Thuslazy GetMyName()
can also be converted to$Code$_Lazy.Value
, where$Code$_Lazy
is an generated Lazy instance usingnew Lazy(()=>GetMyName())
.LazyInitializer
andLazy<T>
are internally different, and lazy expression may choose one of them, or even provide a new implementation (e.g. providing a new value type namedSystem.ValueLazy<T>
if we considering a value type object will take advantage of efficiency and memory management).Lazy Expression Object
Lazy Expression may be recongnized as two different forms: (1) Simple Right-Value with the same type of lazyed values, e.g.
lazy 1
is recongnized as right value expression with type int. (2)Lazy<T>
or other equivlent wrapped type, e.g.lazy 1
will be considered as typeLazy<int>
in compiling time. The second implementation may cause a lot of addtional problems such as operator overload promotion, nested lazy expression handling, etc. It may also increase the cost for lazy evaluating implementation since developers may have to useLazy<T>.Value
to access the real value. However, wrapped type may also introduce some benefits for more complex scenes as shown below.An alternative handling method similar with string interpolation also can be considered, which means the lazy expression can be explained as either type
T
or typeLazy<T>
, according to the declared type during assignment. If implicit type infering using keywordvar
is used, the default behavior is recgonized as typeT
. The following code snippet will illuminate this design:Value Maintenance
Sometimes we may want to discard, update or explicit evaluating the lazy expressions. Under such circumstance,
Lazy<T>
may provide this related features such asUpdate
,Discard
, andEvaluate
methods, as theDiscard
method may internally reset the state flag to make effect for example. For simple designed lazy expressions (The first design method in the previous section), compiler has to introduce addtional keywords for these features, or just drop these advanced features in order to keep the syntak simple.State and SyncLock Sharing
The EnsureInitialized method requires both a state flag and a sync-lock object. In some situations, library designer may share state or sync lock between objects. The
Lazy<T>
type in the 2nd design method may provide some configure methods. However, the lazy expression shoudl not be designed for everything, while introducing these features may enhance the difficulty in compiler design, and thus it may not suitable for implementing them.Serialization, etc
Infrastructure for lazy expressions should be carefully considered, and certain CLR level support may also needed.
Beta Was this translation helpful? Give feedback.
All reactions