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

Shared Immutable Properties #3831

Closed
ghost opened this issue Aug 31, 2020 · 11 comments
Closed

Shared Immutable Properties #3831

ghost opened this issue Aug 31, 2020 · 11 comments

Comments

@ghost
Copy link

ghost commented Aug 31, 2020

Closed as the auto-implementation only version of this has actually been implemented in C# 9's init props, and the manual implementations were sugar for unknown use-cases.

Updated to describe implementation & the why better, reflecting comments on 05/09/2020

When declaring a virtual or abstract (for brevity, will simply be defined as virtual from here on out) property, be it on a class or an interface, it is currently impossible to express that the property should be immutable.

When you declare a non-virtual property, or a sealed property, you can do this:

public string Foo { get; }
public sealed override string Bar { get; }

public sealed class Baz
{
    public override string Bar { get; }
}

All of the above properties are immutable. There is no way to change their values (barring reflection, but maybe not on .NET Core 3+ I think?) once they are initialized. However, a property only having a getter is never a good indication that it is immutable.

private string _foo;
public string Foo =>  _foo; // Not immutable.

private readonly string _bar;
public string Bar => _bar; // Immutable.

This means that for anybody to know whether a given property is actually immutable or not, they must know that the property is either auto-implemented, or pointing at constants, pure methods, other immutable properties, or readonly fields at its final implementation.

This is important for all kinds of reasons, but the most common ones are parallelism and mapping (what if you want to use Bar or Foo as a dictionary key?). Knowing that a value is immutable is incredibly handy. If you don't know that a value will always be immutable, however, you need to handle its mutability in many situations... This means unnecessary locking, event handlers for a property important to a mapping being changed, etc.

The only way you can enforce an immutable value on a shared contract currently is to have access to its base class, and be able to mark the value as sealed, and point it at an immutable value (or auto-implement it with only a getter). Interfaces are out, which means structs and a huge swathe of classes cannot guarantee immutability for their values in a shared context.

Enter immutable (or any other keyword) for properties:

public interface IFoo
{
    immutable string Bar { get; }
}

public abstract class Baz
{
    public immutable string Quz { get; }
    public abstract string Quux { get; }
}

public class Corge : Baz, IFoo
{
    public immutable string Bar { get; }
    public override immutable string Quux { get; }
}

Not tied to the syntax; the immutable keyword could be readonly, or a no set could be used, or anything else.

There is a fairly obvious, incremental implentation path for this feature. The simplest is to only allow auto-properties when implementing an immutable property. This pretty much solves the problem (I'm sure there are edge cases which could be addressed (i.e side-effects irrelevant to the return value), but you don't lose anything with this implementation).

However, for explicit get implementations:

  • Allow referencing constants.
  • Allow referencing readonly fields.
  • Allow referencing other immtuable properties.
  • Make the above apply only to expressions relevant to the return value of the getter (allow side-effects which don't change the return value).
  • Implicitly mark all non-virtual properties which only have an auto-getter as immutable.
  • Implicitly mark all non-virtual properties which only have a getter whose body matches all the criteria as immutable.
  • Above but for sealed props, or props on sealed classes overriding a virtual property on their base.

Could do this bullet-by-bullet, or chunk the first three, or do it all at once. Each point is only a nice-to-have, though the first four, at least, are pretty important if there are real use-cases for side-effects not changing the prop's return value (not convinced, personally).

To use a method in an explicit get implementation, we would need to know that the method is pure. The compiler does not check purity, and relying on an attribute when everything else is checked at compile time seems like a bad idea. However, this implementation actually lays a lot of the groundwork for compile-time checking pure methods.

Collisions between properties declaring a setter and properties explicitly not declaring a setter on interfaces can be handled in the same way as any other interface collision:

public interface IFoo
{
    immutable Guid Bar { get; }
}

public interface IBaz
{
    Guid Bar { get; set; }
}

public class Qux : IFoo, IBaz
{
    Guid IBaz.Bar { get; set; }

    public immutable Guid Bar { get; }
}

As a final note, pretty much all of the responses have been negative so far, but with the hold-up being the ability to effectively declare the lack of a method on an interface. Interfaces just aren't the place for this, right?

I do not see the alternative.

  • Immutable classes won't solve the sharing issue. The interfaces they implement still aren't explicit.
  • Shapes don't look to be including fields, and given that they also are more focused on providing common interfaces, not the implementational details, the same 'argument' applies to them.
  • Marking an entire interface as immutable would violate the same 'rule' while offering a less granular solution for no good reason.
  • Implementing a brand new concept exactly like interfaces except you can mark properties on them as immutable would make absolutely no sense.

If you really hate the idea of interfaces being able to do this, please actually suggest any better solution (or argue that this isn't a problem at all). The actual description of interfaces in the documentation is as follows:

An interface contains definitions for a group of related functionalities that a non-abstract class or a struct must implement.

There is nothing in the documentation that I can see explicitly ruling anything like this out. Interfaces are, as of C#8, no longer mere lightweight contracts. They can declare default implementations. Being able to declare a functionality of provides this immutable value is far less in conflict with the idea of light-weight behavioural contracts than plenty of things interfaces can already do... Interfaces can provide values. The mutability of a value is vital information when consuming that value. The argument is pretty much "we shouldn't be able to do this because we can't currently do this".

@svick
Copy link
Contributor

svick commented Aug 31, 2020

I'm not sure this proposal would be worthwhile on its own. Instead, what could make sense is a more comprehensive solution for working with immutable types or objects. (See some previous discussion at #2543.)

@HaloFour
Copy link
Contributor

IMO an interface describes a contract of what the implementing type can do, not what it can't. It wouldn't make sense for the interface to dictate that a setter doesn't exist anymore that it would for any arbitrary method to not exist.

@ghost
Copy link
Author

ghost commented Aug 31, 2020

IMO an interface describes a contract of what the implementing type can do, not what it can't. It wouldn't make sense for the interface to dictate that a setter doesn't exist anymore that it would for any arbitrary method to not exist.

I really don't think declaring that some arbitrary method does not exist really is comparable to declaring that a property doesn't have a setter.

Properties might happen to be implemented via methods, but their purpose is to retrieve a value, not to do something. Whether a given parameter can change at any time is relevant to a significant number of scenarios in which you might want to consume it.

As it currently stands, unless your interface is internal, and you have complete control over every single implementation of it, the only safe way to consume an interface is to assume that every property on it is mutable. Sure, you can write documentation to tell people implementing that interface not to make a given property mutable, but you could also write documentation to tell people not to use the setter on a property, or not to change a field, and still we have readonly fields and get-only properties.

Interface properties are the only way to define values common to different structs, or any classes which need multiple-inheritance (or that you can't change the base class for). Mutability is fundamental to how you handle all sorts of things like threading, mapping, caching, etc. Interfaces are a set of member definitions (and now, sometimes, implementations) to provide common functionality. Whether or not a value is mutable is part of the definition of a value.

@spydacarnage
Copy link

spydacarnage commented Aug 31, 2020 via email

@HaloFour
Copy link
Contributor

@iBeizsley

I really don't think declaring that some arbitrary method does not exist really is comparable to declaring that a property doesn't have a setter.

Except that's exactly what it is.

Properties might happen to be implemented via methods, but their purpose is to retrieve a value, not to do something.

It's up to the implementation to determine what a property does or how it does it. The getter accessor method may mutate the value by itself. Even if an interface could prevent a type from providing a setter accessor that wouldn't prevent the type from providing some other mutator method.

Mutability is fundamental to how you handle all sorts of things like threading, mapping, caching, etc. Interfaces are a set of member definitions (and now, sometimes, implementations) to provide common functionality. Whether or not a value is mutable is part of the definition of a value.

I am not challenging any aspect of these statements. I would like to see more and better immutability support in the language and runtime. I don't think that interfaces are the place to try to dictate them.

@CyrusNajmabadi
Copy link
Member

The above gives a compiler error, because there's no overridable set accessor. There's no technical reason it couldn't behave like an interface here (you can just point it at a local field, and add a set method yourself).

However, the above doesn't force hte thing you want. i.e. it doesn't force anything to be immutable. You could still override a get-only property (or method), and absolutely have it mutate.

@ghost
Copy link
Author

ghost commented Sep 2, 2020

However, the above doesn't force hte thing you want. i.e. it doesn't force anything to be immutable. You could still override a get-only property (or method), and absolutely have it mutate.

I'm aware; that's what the part in brackets states. I'm really just pointing out the weird inconsistency there. On abstract classes, you can't define a setter on a property which doesn't have a setter defined on its base. For interfaces, you can. While you can get around that (as in it doesn't actually guarantee immutablility), the actual way to declare an immutable property on an abstract class is the same for a normal class... You don't make it virtual, and don't give it a setter.

Anyway:

The only argument against this seems to be that interfaces 'just aren't the place for this', but I don't really see the alternative...

immutable method parameters? That only enforces that a given method won't change anything on the parameter.

immutable on a class or struct? Now the entire thing is immutable; you can't mix and match... And, if you need to use an interface to refer to multiple immutable classes, consumers of that interface don't know that the class is immutable.

An immutable generic constraint would make ensuring all of the places you care about immutability for an interface property possible, so long as you can use a generic method to do whatever it is you're doing, but again, that's the entire class/struct being immutable, not just the properties you actually need to be immutable.

Depending on how the immutability implementation works, you might be able to have a property on the immutable class which contains the mutable fields for that class. The other way round won't work, because you'll need a property to expose the immutable member wrapper, and that property has no guarantee of immutability when consuming it via an interface.

Shapes might work, but unless you can declare fields on shapes, you're back to the same old problem (unless you can declare properties as immutable for all implementations, you can't declare something as actually immutable... and if you can declare a property as immutable for all of its implementations, then why not on an interface?).

Perhaps I've framed this all wrong, but it's really a question of whether you can be explicit in defining whether a property is immutable. On a struct, or a class where you don't make the property virtual, you can; not declaring a setter declares the property as immutable. But the only time you can share the fact that a property is immutable is when you have a base class which has a non-virtual property with only a getter on it, hence the focus on interfaces... Because interfaces are the preferred way of defining a common set of members, and the only way of doing so in many scenarios.

Unless you're going to invent a new way of having a common set of values which works just like an interface, except you can declare properties on it as immutable, then nothing will solve this... And that seems like a vastly inferior option to just letting you do it on an interface.

@spydacarnage
Copy link

Even if you could have your immutable descriptor, I still don't see how putting it in an interface will help you enforce what you want.

Say that this is valid:

public interface IFoo
{
	immutable int Bar { get; }	
}

While I could implement it like this:

public class Baz : IFoo
{
	public int Bar { get; }
	public Baz(int startingValue) => Bar = startingValue;
}

There is still nothing to stop me implementing it like this:

public class Fail : IFoo
{
	public int Bar => new Random().Next(1, 101);
}

I still meet the requirements of the interface - I've not declared a setter, but the output of the property still isn't what I would consider "immutable".

@ghost
Copy link
Author

ghost commented Sep 2, 2020

There is still nothing to stop me implementing it like this

The compiler can stop you implementing it like that. As mentioned in the OP, there's an obvious progression for implementing such a descriptor.

The simplest is to only allow auto-properties to implement an immutable property. This pretty much solves the problem (I'm sure there are edge cases which could be addressed (i.e side-effects irrelevant to the return value), but you don't lose anything with this implementation).

However, for explicit get implementations:

  • Allow referencing constants.
  • Allow referencing readonly fields.
  • Allow referencing other immtuable properties.
  • Make the above apply only to expressions relevant to the return value of the getter (allow side-effects which don't change the return value).
  • Implicitly mark all non-abstract/virtual properties which only have an auto-getter as immutable.
  • Implicitly mark all non-abstract/virtual properties which only have a getter whose body matches all the criteria as immutable.
  • Above but for sealed props, or props on sealed classes overriding a virtual property on their base.

Could do this bullet-by-bullet, or chunk the first three, or do it all at once. Each point is only a nice-to-have, though the first four, at least, are pretty important if there are real use-cases for side-effects not changing the prop's return value (not convinced, personally).

While this is primarily focused on interfaces, there's no reason you couldn't also allow immutable on virtual/abstract props defined in classes too (even when overriding a non-immutable property defined on a base class).

Down the road, proper pure methods could pretty much just build on this logic, and then be allowed in immutable property getters too. But that's a different story...

@ghost ghost changed the title Immutable Interface Properties Immutable Properties Sep 5, 2020
@ghost ghost changed the title Immutable Properties Shared Immutable Properties Sep 8, 2020
@ghost
Copy link
Author

ghost commented Sep 11, 2020

After testing init props in C# 9, it appears they behave in exactly the same way as the auto-implementation version of this. While it doesn't cover manual implementations like in this issue, those were more an afterthought for use-cases I couldn't think of, and if they'd actually be useful, it'd make more sense to propose them separately. Will point out the only argument against them raised here doesn't really stand up now that you can declare an interface prop as init only, thus declaring that there is no setter method...

public interface IFoo
{
    string Bar { get; init; }
}

public class Baz : IFoo
{
    public string Bar { get; init; } // Valid
    public string Bar { get; } // Invalid
    public string Bar { get; set; } // Invalid
    public string Bar => 5; // Invalid
}

@ghost ghost closed this as completed Sep 11, 2020
@333fred
Copy link
Member

333fred commented Sep 11, 2020

Will point out the only argument against them raised here doesn't really stand up now that you can declare an interface prop as init only, thus declaring that there is no setter method...

To be clear, the only reason that we didn't allow you to expand that init to a full set in the class implementation is due to clr limitations, not due to any moral desires around enforcing intent. If we could have allowed implementation 3 in your examples, we would have.

This issue was closed.
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

5 participants