Proposal: Annotate pure methods #1157
Replies: 39 comments
-
At the risk of being a pedant, the other requirement to be a pure method is that it must not be |
Beta Was this translation helpful? Give feedback.
-
The only PureAttribute I know of is in System.Diagnostics.Contracts and I really don’t like the idea of reviving that namespace. Perhaps make a new attribute. |
Beta Was this translation helpful? Give feedback.
-
That attribute is already used extensively in the BCL. I don't think it makes much sense to introduce another one. |
Beta Was this translation helpful? Give feedback.
-
It’s used about 600 times in .NET Framework, but virtually unused in .NET Core outside of System.Collections.Immutable. |
Beta Was this translation helpful? Give feedback.
-
I feel like pure should be a language construct and not an attribute, the compiler, beyond optimization, should throw an error when it tries to access a field, IO or some other global state, finally, pure functions cannot call non-pure functions. In terms of implementation, it's probably obvious that pure functions are static with additional constraints. In terms of APIs, I guess that many APIs today follow the semantics of pure so there shouldn't be a problem changing them. Finally, it might be backward compatible with static functions that are marked with |
Beta Was this translation helpful? Give feedback.
-
Why does pure functions have to be static? The only extra thing to consider is that any base call also has to be pure. |
Beta Was this translation helpful? Give feedback.
-
If it's not static, then surely it has state ( |
Beta Was this translation helpful? Give feedback.
-
Does that matter terribly if the state itself is readonly/immutable? An instance method is really just syntax candy for a static method that accepts the instance as the first parameter, which would qualify for being pure. |
Beta Was this translation helpful? Give feedback.
-
Probably not. I've just always assumed pure functions have to be static without giving much thought to why. |
Beta Was this translation helpful? Give feedback.
-
@DavidArno I would claim that the only thing of importance is that member functions can be virtual, and that's easy to fix: just require that base calls are also pure. Aside from that, the difference between calling a member function and calling a static function with that object passed as the first argument is the access level you get (i.e. the member function will have access to private and protected variables on that object) but that hardly matters for purity. |
Beta Was this translation helpful? Give feedback.
-
I think these two concepts are fairly similar, but we need to annotate a whole bunch of operators/members for it to be actually useful. For example built-in operators on primitives are all both pure and constexpr.. As an example An interesting example that came up on roslyn repo is when you're using an impure type like StringBuilder
What rules we need to define to allow this as a pure function? dotnet/roslyn#7561 (comment) |
Beta Was this translation helpful? Give feedback.
-
That's the challenge with working out what is pure. That method (caveat: "has no side effects" ignores heap allocations, which may be a mistake on my part). |
Beta Was this translation helpful? Give feedback.
-
Sure, you're right. |
Beta Was this translation helpful? Give feedback.
-
@alrz Why would you think that
|
Beta Was this translation helpful? Give feedback.
-
What if a dependency is taken on a particular method being |
Beta Was this translation helpful? Give feedback.
-
Could you elaborate? Much like |
Beta Was this translation helpful? Give feedback.
-
For this particular nail, "pure" is just a big hammer. All that is needed is "readonly" instance methods - methods that promise not to modify the object on which they're invoked. Not that it would be easier to add "readonly" methods... And as @tannergooding already mentioned above, examples involving |
Beta Was this translation helpful? Give feedback.
-
C++ |
Beta Was this translation helpful? Give feedback.
-
There are certainly additional concerns when you have JIT'd code, but for methods that:
The rules of how the IL gets executed are fairly explicit and shouldn't have inputs/outputs differing between runtimes or hosts. So I'll leave it at that, since further discussion should probably be moved to the |
Beta Was this translation helpful? Give feedback.
-
If the method is not in the project you're compiling then there is no IL. That's also why something like pure/readonly needs to be part of the method definition in metadata (an attribute on the method). Otherwise the C# compiler could just look at method's IL and figure out that it doesn't change the object (though having the C# compiler automatically determine that isn't the best way, it's too easy to cause unexpected breaking changes when changing the method). |
Beta Was this translation helpful? Give feedback.
-
Why would i write a mutable vector using properties? That doesn't seem to make sense to me. Especially properties that are no-ops... |
Beta Was this translation helpful? Give feedback.
-
Yes. That's why i literally said: So, if you wanted this, it would likely be wishywashy, just like nullable reference types :) |
Beta Was this translation helpful? Give feedback.
-
Yup. And this was considered and not ruled out. It simply was made out of scope for the initial work being done here. It was felt that the majority of cases early on would suffice with a "readonly struct". That made things simpler overall, and helped the feature fit into tight schedules. More fine-grained readonly-ness is certainly something possible in the future, without going whole-hog into 'purity'. |
Beta Was this translation helpful? Give feedback.
-
I err'ed with this today. I had a It would have been great if C# could warn me about it rather than just silently voiding mutations. |
Beta Was this translation helpful? Give feedback.
-
You'd get that warning for literally every instance method you invoke on a |
Beta Was this translation helpful? Give feedback.
-
If this proposal were championed, In https://github.com/dotnet/csharplang/pull/1165/files
pure interface method could solve this issue. |
Beta Was this translation helpful? Give feedback.
-
That's a pretty tricky case, because we need to introduce two new concepts--in addition to pure. We need the concept of (and I'm sorry, but I can't come up with a better name for this right now) semi-pure. A semi-pure method is one that does not have side effects but does mutate one or more of the method's arguments (or, in OO, the receiver of the instance method). In your example Then we need the concept of ownership. A pure method Our method M can therefore be called pure because: public string M() {
var builder = new StringBuilder(); // Creating objects is okay in a pure method as long as the constructor is pure
builder.Append("Hello world!"); // This method is semi-pure. We are allowed to call it only if we can prove that we **own** `builder`
return builder.ToString(); // StringBuilder.ToString() is already pure and this is fine
} I'm pretty sure that this causes the feature to blow up out of all proportion and is probably not going to be implemented, but it's worth thinking about. |
Beta Was this translation helpful? Give feedback.
-
That is similar to my thoughts. "Ownership" is a good term. If a type has local mutation only (ie mutates only its own state) and the scope of an instance of that type is confined to the method (ie, the method owns that instance) and its final output before going out of scope is an immutable "value", then that method can still be viewed as pure. |
Beta Was this translation helpful? Give feedback.
-
So what's wrong with using C++'s concept of In C#, there is no practical way to guarantee whether a method modifies shared state or not. Even if you create an class Ship
{
Universe* Owner; // Ships have access to Universe
public:
// does Draw use Owner to cause side effects?? what about other member fields??
// No way to tell unless you examine hundreds of lines of code
void Draw()
{ ... }
};
class Universe
{
mutex Sync;
vector<Ship*> Ships;
public:
void KillShips(Predicate<Ship*> shouldKillShip) // called by simulation thread
{
lock_guard<mutex> writeLock {Sync};
utils::erase_if(Ships, shouldKillShip);
}
void DrawShips() // called by UI thread
{
lock_guard<mutex> writeLock {Sync};
for (Ship* ship : Ships)
ship->Draw(); // will this modify Ship state? Will this modify Universe state?
}
}; Because of multi-threaded access, of course, the class requires synchronization. Since there is no way to prove whether So C++ has an extremely simple solution to this. It doesn't try to be perfectly pure. Most applications don't care about purely pure functions. Developers think "If I call this method, will it change the internal state? I don't want that to happen, otherwise, this other thread will crash."
Since Now, what are the benefits? We can greatly improve performance: class Ship
{
Universe* Owner; // Ships have access to Universe
public:
// const propagation: Owner is implicitly `const Universe*` so we have a very good guarantee
// Draw won't cause dangerous side effects or dead-locks; only const methods may be called
// With just one glance at `const`, you can be reassured
void Draw() const
{ ... }
};
class Universe
{
mutable shared_mutex Sync; // we don't care if mutex state is modified (lock, unlock)
vector<Ship*> Ships;
public:
void KillShips(Predicate<Ship*> shouldKillShip)
{
lock_guard<shared_mutex> writeLock {Sync}; // exclusive lock
utils::erase_if(Ships, shouldKillShip);
}
void DrawShips() const
{
shared_lock<shared_mutex> readLock {Sync}; // allow multiple threads to read
// const propagation: Draw must be const.
// due to that, we know it's safe to parallelize this code! this gives a huge perf boost!
parallel_for(Ships, [](const Ship* ship) {
ship->Draw(); // safe and enforced by the compiler!
});
}
}; Huge performance boost thanks to immutability guarantees. If you do this in a language without To sum up, this whole discussion about perfect pureness is counterproductive to real-world use cases. Simply having |
Beta Was this translation helpful? Give feedback.
-
A big plus one on the C++ const method approach - I think this is severely lacking in C#. I'd only change the keyword to be One example of where this is useful, is the defensive copy problem when invoking a method on a readonly field which is a struct. If methods could be flagged as Similarly, if a non-readonly struct is passed via an I'm sure there are other possible advantages for this - hope it gets picked up. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
I recently learned (in #1155 ) that C# will create a copy of structs if you use
in
since it can't trust that the code it calls on them really does not mutate the struct.So I have a proposal: make C# respect
System.ComponentModel.PureAttribute
System.Diagnostics.Contracts.PureAttribute
(or similar), and add this attribute itself if methods do not mutate state (I.e. write to variables, with the exception of constructors, or call other non-pure methods). The advantage here is that the user can additionally annotate with[Pure]
even if the method does mutate (such as caching the result).Advantages:
Disadvantage:
Why would this help?
If methods are pure, there is no reason for the compiler to produce a copy of the struct. It can just use regular good old
ldloca
instead of copying the value. It only needs to produce a copy if the method that gets called is non-pure.Beta Was this translation helpful? Give feedback.
All reactions