Git Product home page Git Product logo

Comments (64)

GSPP avatar GSPP commented on August 15, 2024 8

This proposal has quite a big concept load and language surface. Shouldn't immutability as a convention, as a comment or as an attribute plus an analyzer be a sufficient solution as well? I do see the advantages of checked immutability but they seem fairly minor compared to being immutable by convention.

I use a lot of immutability and I find that the immutable by convention approach works extremely well.

Are those annotations intended to make work in a team easier? I can see that happening but I also see that if team members do not "see" that the class is immutable (when it clearly looks like it) then they likely don't have a good grasp of immutability at all and just make that think mutable. I have worked with such people a lot. It's the kind of people that wrap a null ref exception in catch {} instead of fixing the root cause.

I'm not sure the language should enforce coding patterns much. It should make immutability easy but it already does that I think.

Also, maybe we'll have a different understanding of immutability 5-10 years from now (just as 10 years ago the .NET Framework designers had a weak understanding of that and their patterns are no longer our patterns necessarily). Patterns change over time. Not sure it's wise to bake them into the language.

from csharplang.

soroshsabz avatar soroshsabz commented on August 15, 2024 4

ITNOA

Why not use public readonly class C instead of public immutable class C? this solution does not need add new keyword such as immutable to language.

One other issue is that I think if we looked to each class for two-sided (mutable side and immutable side) is much more effective. It can easily happen by adding the ability to specify method, property and operator that guarantee the state of class does not change (similar to right const in C++). With this feature we do not have write two class (one mutable and another immutable) for each entity.

Although if we want write a class that have only immutable side, readonly class feature is useful to reduce extra writing right const (or similar keyword) for each method and ...

from csharplang.

dbolin avatar dbolin commented on August 15, 2024 4

I was surprised to find this issue has existed for more than four years and I still couldn't find any Roslyn analyzer for immutable types, based on attributes or otherwise. Newer projects I work on use immutable types more and more, and while so far immutability by convention only has worked, it would be nice to have an analyzer to be sure you don't miss anything or otherwise accidentally fail to make a type truly immutable when you intended to.

So I went ahead and made a basic implementation based on attributes - you can find the code here if you want to try it out. I tried applying it to one of the projects I'm working on and it seemed to work out... it actually ended up finding some types near the bottom of an immutable type tree that were mutable, but fortunately nothing ever actually modified the instances of those types after construction.

I think I prefer the analyzer approach to a language feature because you can always disable the analyzer for the few cases where you need mutability (for performance or other reasons), but everywhere else still can have the constraints of immutable types applied.

from csharplang.

McZosch avatar McZosch commented on August 15, 2024 3

The problem with writing immutable types is having a redundant private field list and constructor signature, both required to be synced with the immutable property list of a class. This is a pain in the ass.

What do we really need to overcome this?

The behavior we require is to have certain properties and members only be set once at object creation. For such cases, there is currently no keyword / attribute. My proposal is to have keyword or attribute "initial", which declares code only to be consumed during object creation.

public class Person
{    
    public string FirstName { get; initial set }
}

The compiler would allow the following

var person = new Person { .FirstName = "James" };

but disallow any following access like

person.FirstName = "Jim";

This would also work for mixed scenarios and would allow for methods or getters be available only at object creation time (initializers!). Finally, it would make designing immutable types only a minor variation of the mutable variant.

public class Person
{    
    public string FirstName { get; set }
}

What would additionally be really nice is to have better modification support.

var person1 = new Person { .FirstName = "James" };
var person2 = modify person1 { .FirstName = "James" };

This would kill the necessity to have the same immutable property pain at the consumer side. Maybe this could be a more concise way to clone ordinary objects, too!?

from csharplang.

AlexRadch avatar AlexRadch commented on August 15, 2024 3

I suggest to use val for immutable variables.

val x = 10;
val string s = "some string";

int SomeFunction(val List<int> source); // can not change list
{
    val c = source.Count(); // correct
    source.Add(5); //  generate compiler error
}

class List<T>
{
   immutable int Count(); // can not change self
   void Add(T item); // can change self
}

(var int X,  val int Y) tuple1 = (10, 20); // Tuple with mutable X and immutable Y
var tuple2 = (var X:10, val Y:20); // Same tuple with mutable X and immutable Y

from csharplang.

sanisoclem avatar sanisoclem commented on August 15, 2024 3

If may I propose a different solution:

Immutable variables, fields, return values and parameters

If we define mutating as:

  • modifying a field object.field = something
  • calling any method that modifies a field object.setField(something)

We can have mutable references (not sure what to call it) that will only allow actions that will not mutate that object (compile time checked).

Immutable return values

class Person
{
    public string Name { get; set; }
    public string Email { get; set; }
    public void Mangle() { Name="mangled"; }
}

public static class PersonFactory
{
    immut Person GetImmutable(); // explicitly tells the consumer that the return value is immutable
}

We will have the following results:

var p = PersonFactory.GetImmutable();
p.ToString(); // success
string name = p.Name; // success
p.Name = "something"; // will not compile
p.Mange(); // will not compile

Propagation

To be consistent, immutability should propagate to any fields and return values.

class Person
{
    public string Name { get; set; }
    public string Email { get; set; }
    public Car Car { get; set; }
}
class Car
{
    public string Make { get; set; }
}

public static class Program
{
    public static void Main()
    {
        var p = PersonFactory.GetImmutable();
        string make = p.Car.Make; // success
        p.Car.Make = "something"; // should not compile
    }
}

Other examples

class Person
{
    readonly Data _otherData = new Data(); // can be mutated within the class

    public string Name { get; set; }
    public immut string Email { get; set; } // has no effect since string is already immutable
    public immut Car Car { get; set; } // Car cannot be mutated outside or within person

    public immut Data GetData() // returned instance of data cannot be mutated even if the private instance can be
    {
        return _otherData;
    }

    public static void Inspect(immut Person person) // person cannot be mutated within method
    {
        person.ToString();
        //person.Name = "something"; // will not compile
    }
    public static void Mutate(Person person) // a mutable parameter is required even if we are not actually mutating
    {
        person.ToString();
    }
}
class Data
{
    string data;
    public void Mutate()
    {
        data = Guid.NewGuid().ToString();
    }
}
public static class Program
{
    public static void Main()
    {
        var person = new Person();
        var immutPerson = (immut Person) person;

        var data = person.GetData(); // returned data is immutable even if person is mutable
        data.Mutate(); // will not compile

        person.Name = "something";//ok
        immutPerson.Name = "something"; // will not compile

        Person.Inspect(person); // ok
        Person.Inspect(immutPerson); // ok

        Person.Mutate(person); // ok
        Person.Mutate(immutPerson); // will not compile

        person.Car = new Car(); // ok
        immutPerson.Car = new Car(); // will not compile

        immutPerson.Car.Mutate();// will not compile
        person.Car.Mutate();// will also not compile
    }
}

Ownership

Immutability can also be used to express ownership.

Disclaimer

Most of this is based off rust's &mut references (which plays a big part in its awesome Ownership and borrowing mechanics). It would really be awesome if something similar can be implemented in C# (that and explicit nulls)

from csharplang.

sharwell avatar sharwell commented on August 15, 2024 2

@stephentoub I currently believe all of your proposed functionality can be provided by a combination of the following items:

  1. A new ImmutableAttribute type which can be applied to classes and structs.
  2. A diagnostic analyzer which ensures the conditions above are met.

Since the attribute is trivial, I'll explain my point regarding the analyzer.

  • Fields must be readonly: Easy to validate with an analyzer
  • Restricted use of this in a constructor: Easy to validate with an analyzer
  • Base type must be object, whitelisted (e.g. String), or Immutable: Easy to validate with an analyzer
  • Derived types must have [Immutable]: Easy to validate, but each project must have the analyzer enabled

The fun part is generic type constraints. As it turns out, you probably don't actually need them.

  • Consider this code:

    public interface Foo<T>
      where T : immutable
    {
    }

    Here, the constraint is pointless because T could only be mutated if it provides mutating operations, which it doesn't because it's immutable.

  • Consider this code:

    [Immutable]
    public class ImmutableTuple<T1, T2>
      where T1 : immutable
      where T2 : immutable
    {
      public T1 Item1 { get; }
      public T2 Item2 { get; }
    }

    In this code, the constraints are still not necessary, because the static analyzer could do one of the following:

    • Infer the immutability requirement from the use of T1 and T2 as fields of the object, and enforce the requirement at the point where ImmutableTuple<T1, T2> is instantiated.
    • Simply require that the generic type arguments to a type marked [Immutable] must also be immutable.

    Not that I have additional work to convince myself that an analyzer could accurately propagate this condition across multiple generic types.

from csharplang.

sharwell avatar sharwell commented on August 15, 2024 1

A Roslyn-based analyzer and a new [Immutable] attribute would support compile-time checking of this feature, provided certain types in core .NET assemblies where whitelisted in the implementation.

The biggest concern I would like to see resolved prior to implementing this is how we handle the builder pattern as cleanly (in my opinion) as the WithHeaders method I added here:

sharwell/openstack.net@a09fc52

For this implementation (specifically for StorageMetadata.WithHeadersImpl), I had to make two fields mutable even though the type remained immutable to the outside world.

from csharplang.

Clockwork-Muse avatar Clockwork-Muse commented on August 15, 2024 1

Currently, when you access a readonly field in a method, it (in IL, mostly transparent to the developer) copies it to a local variable before doing anything with it. This constitutes a performance penalty.
There's also this gem, buried in System.Collections.Immutable's array:

/// This type should be thread-safe. As a struct, it cannot protect its own fields
/// from being changed from one thread while its members are executing on other threads
/// because structs can change *in place* simply by reassigning the field containing
/// this struct. Therefore it is extremely important that
/// ** Every member should only dereference <c>this</c> ONCE. **
/// If a member needs to reference the array field, that counts as a dereference of <c>this</c>.
/// Calling other instance members (properties or methods) also counts as dereferencing <c>this</c>.
/// Any member that needs to use <c>this</c> more than once must instead
/// assign <c>this</c> to a local variable and use that for the rest of the code instead.
/// This effectively copies the one field in the struct to a local variable so that
/// it is insulated from other threads.

...which means currently implementing an "immutable" type is more subtle and fraught with danger than people may be aware (heisenbugs are likely to arise in client applications if they aren't aware of this issue).

I don't care too much about the particular end syntax used (whether we get a new keyword, or redefine the semantics of readonly, or whatever), but is the end result likely to fix these two issues?

  1. immutable fields shouldn't need to be copied before use, if the intent is that they can't be changed (transparently or otherwise).
  2. structs should receive equivalent protection to classes, so that this (and internal data!) can be safely referenced without issue...

from csharplang.

MgSam avatar MgSam commented on August 15, 2024 1

@stephentoub Have you seen Neal's proposal for pattern matching and records in C#? I think it addresses a lot of your concerns here.

from csharplang.

sharwell avatar sharwell commented on August 15, 2024 1

@jaredpar Overall I do agree with your comments. However, I also believe that some burden is placed on development teams to incorporate best practices described by libraries they depend on. There are all sorts of ways they can violate preconditions of libaries. One obvious example is Dictionary<TKey, TValue>, where post-conditions of methods and and invariants for instances can be violated by the implementation if the user does one of the following:

  1. Uses an IEqualityComparer<TKey> (including the default if not specified) which produces different hash codes for the same object.
  2. Performs operations from multiple threads, where at least one of those operations is a mutating operation on the data structure.

While the first item would be challenging to prevent, it would be easy to address the concerns for the second point by synchronizing concurrent access to the object.

If immutable types were provided via optional static analysis, it is conceivable that many users would be able consume them even without an analyzer because they are only prone to failure in very specific ways:

  1. Extending an immutable type and adding mutable properties.
  2. Supplying a mutable type for the generic argument to an immutable type, where that type argument becomes a property of the immutable object.

There are other ways to improve overall reliability, such as declaring a dependency on the analyzer package when creating a NuGet package for a library which defines immutable types. It would also be possible to package the ImmutableAttribute itself in the same NuGet package that provides the static analyzer.

For those wondering why I would push for an analyzer instead of a language change:

Concurrency remains a major challenge for modern application development. Synchronization constructs such as lock (or synchronized in Java) provide only limited solutions to these problems, especially when it comes to scale. One alternative approach is leveraging lock-free concurrent structures like those in System.Collections.Concurrent. Another approach is providing immutable data representations which are backed by data structures that support efficient transformation, such as those in System.Collections.Immutable.

For better or worse, these libraries do not provide out-of-the-box support for every scenario an application developer might encounter. Improving the ability of developers to extend these concepts will go a long way towards improving overall developer efficiency when creating reliable, scalable applications intended for concurrent environments.

In my opinion, the best approach would be to first implement this as an analyzer so people can start using it, and later consider incorporating it into the language and/or runtime. When you consider that several parts of the C# syntax, such as the out or params keywords applied to parameters, compile down to nothing more than applying an attribute (OutAttribute and ParamArrayAttribute in these cases), it's reasonable to think that a new immutable keyword for a class declaration could compile down to applying the ImmutableAttribute automatically. The only major change would be incorporating the analyzer into the standard compiler instead of distributing and enabling it separately.

from csharplang.

jaredpar avatar jaredpar commented on August 15, 2024 1

The only major change would be incorporating the analyzer into the standard compiler instead of distributing and enabling it separately.

This is the crux of the issue for me. Immutable should mean immutable. I shouldn't have to think about it, be watching your commit history to make sure that you haven't change things. I should type immutable and get the expected behavior. Analyzers just don't provide that for me.

from csharplang.

jods4 avatar jods4 commented on August 15, 2024 1

The main issue with immutable types is how you create them. It's clumsy to efficiently create copies with multiple different fields, to create cycles in immutable object graphs and so on...

I propose the idea of mutation contexts:

  1. Immutable classes may have non-readonly fields. Setting a field outside of a mutation context is forbidden.

  2. Immutable classes may have property setters. A setter is a mutation context. Calling a setter from outside of a mutation context is forbidden.

  3. Immutable classes may have methods that are a mutation context. Calling such a method from outside a mutation context is forbidden. The syntax to designate such a method must be decided, but it could simply be an attribute.

    public immutable class C
    {
      public int X;
    
      [Mutates]
      public void Increment()
      {
        X += 1;  // Setting a field is OK because this method is a mutation context.
      }
    }
    
    new C().Increment(); // Error: Increment() can't be called from outside a mutation context.
  4. Functions can designate that they may mutate the state of one immutable parameter (syntax to be defined, maybe with an attribute). Such functions are a mutation context for the specified parameter and cannot be called from outside a mutation context.

    static class Utils
    {
      public static void Increment([Mutates] C c)
      {
        c.X += 1;  // OK because this method is a mutation context for c
      }
    }
    Utils.Increment(new C());  // Error: Utils.Increment can't be called from outside a mutation context.
  5. A new keyword mutate that has similar syntax as using creates a mutation context for one variable. This is the only way to establish a new mutation context and none of the other constructs 1-4 can be called outside of it.
    This is the really unsafe part of the code but it can be very useful. Typically, mutating a variable before you have "published" it for external readers is totally safe.
    Because an important use-case for immutability is concurrent programming, I suggest that a memory fence is added at the end of a mutate block. This will guarantee that once readers get the reference to the immutable instance, all its mutations are committed to memory and visible to all cores.

immutable class Person()
{
  public Person dad, mom;
  public Person[] children;  // This is an error, it should be ImmutableArray, I simplified a little bit.
}

static Person CreateSomeone()
{
  // This is dangerous but totally safe on non-shared new variables  
  mutate(var child = new Person())
  mutate(var dad = new Person())
  {
    // Could be inside the mutate as above, but to illustrate different syntax
    var mom = new Person();
    mutate(mom)
    {
      mom.children = dad.children = new[] { child };  // Should be ImmutableArray
      child.dad = dad;
      child.mom = mom;
      return child;
    }
  }
}

// It would be nice if we could also use Initializer syntax, either like this:
mutate(var x = new Person { dad = new Person(), mom = new Person() })
mutate(var d = x.dad, m = x.mom)  // Multiple values? probably not because inconsistent with using...
{
  d.children = m.children = new[] { x };
  return x;
}

// Or maybe like this, because initializer inside mutate() can be a very long expression
mutate(Person x)
{
  x = new Person 
  {
    dad = new Person(),
    mom = new Person()
  };
  mutate(var d = x.dad, m = x.mom)
    d.children = m.children = new[] { x };
  return x;
}

The code above shows how easy it would be to perform any operation on an immutable class that has not been shared yet. But once we are outside the mutate block it's safe. The only "risk" is to misuse a mutate block after an immutable class has been "published". I think that's acceptable (devs will always find their way to abuse, if only by reflection or unsafe code).

Note that to provide really strong guarantees any mutation context (either mutate block, method or setter) must be forbidden to store a reference to the immutable class or one of its Reference members into a static field or a capture context. I don't know if this should be enforced by compiler or is something that the dev must be responsible for.

I thought about using that as a replacement for the unsafe proposition in the issue, but it doesn't really work. The problem is that once you have (private) mutable members as an implementation detail, you pretty much have to wrap all your code with mutate(this). Even a getter may mutate a non-immutable class :(

from csharplang.

leppie avatar leppie commented on August 15, 2024 1

I would like to see this extended to generics too, for example:

void Foo<T>(T t) where T : immutable { ... }

Alternatively, go the C++ const way like

void Foo<T>(immutable T t)  { ... }

PS: Sorry if this has been mentioned.

from csharplang.

bitzeal-johan avatar bitzeal-johan commented on August 15, 2024 1

I guess they refuse to add it to force us to use F# :)

from csharplang.

dsinghvi avatar dsinghvi commented on August 15, 2024 1

A bit adjacent, I'm wondering if there is a C# equivalent for https://immutables.github.io/ ?

from csharplang.

dsinghvi avatar dsinghvi commented on August 15, 2024 1

C# has source generators, which can be combined with partial types to emit the boilerplate code for such a type.

@HaloFour Got it -- is there a source generator that the community generally uses to build immutable types?

from csharplang.

scalablecory avatar scalablecory commented on August 15, 2024

General idea is a good one. I believe it will require CLR support to enforce across languages.

Can we rework how "unsafe" works? This keyword has specific connotations around memory safety that I'm not sure I like diluting. I also think it may be safer and better self-documenting if it is specified on specific fields. Something like:

immutable struct ImmutableArray<T>
{
    readonly mutable T[] array;
}

Though a "readonly mutable" sounds a little funny.

from csharplang.

sharwell avatar sharwell commented on August 15, 2024

All fields are made implicitly readonly

I would prefer a requirement that fields be marked as readonly.

Additionally, the constructor of the type would be restricted in what it can do with the 'this' reference, limited only to directly reading and writing fields on the instance

I would prefer this be a warning. While unlikely and generally not recommended, it's hard to state deterministically that no one will need to be able to write code like this.

from csharplang.

svick avatar svick commented on August 15, 2024

Additionally, the constructor of the type would be restricted in what it can do with the 'this' reference, limited only to directly reading and writing fields on the instance

I would prefer this be a warning. While unlikely and generally not recommended, it's hard to state deterministically that no one will need to be able to write code like this.

Maybe if you marked the constructor as unsafe, this kind of code could be allowed? (I also dislike using unsafe this way, but mutable on constructor would make even less sense.)

from csharplang.

svick avatar svick commented on August 15, 2024
public class Person
{
    public Person(string firstName, string lastName, DateTimeOffset birthDay)
    {
        FirstName = firstName;
        LastName = lastName;
        BirthDay = birthDay;
    }

    public string FirstName { get; } = firstName;
    public string LastName { get; } = lastName;
    public DateTime BirthDay { get; set; } = birthDay; // Oops!

    public string FullName => "\{FirstName} \{LastName}";
    public TimeSpan Age => DateTime.UtcNow โ€“ BirthDay;
}

In this and the following examples, you're using property initializers. I think they shouldn't be there.

from csharplang.

stephentoub avatar stephentoub commented on August 15, 2024

Oops, thanks, @svick. Fixed.

from csharplang.

stephentoub avatar stephentoub commented on August 15, 2024

@sharwell, that's true, but I'd previously written these examples using primary constructors, and without primary constructors, what I'd written didn't make sense. Instead I'm initializing those fields in the regular constructor.

from csharplang.

stephentoub avatar stephentoub commented on August 15, 2024

@sharwell, strange, I could have sworn I was responding to a comment you'd written... it's almost as if it was there and then someone deleted it ;) Oh well.

from csharplang.

sharwell avatar sharwell commented on August 15, 2024

@stephentoub I'd love to get your feedback regarding the commit I mentioned above. In particular, the changes to StorageMetadata.cs and the new file StorageMetadataExtensions.cs.

The intent is for a user to be able to go var newItem = item.WithProperty(value), and have the static type of newItem match the static type of item, even if WithProperty is defining a property on one of that types base classes.

from csharplang.

jaredpar avatar jaredpar commented on August 15, 2024

@sharwell

Sure, an analyzer could absolutely be used here to enforce these rules. In fact it can even be done as a unit test with reflection. I've actually written such code in past projects.

I don't think an analyzer is the right solution here though. Analyzers are great at enforcing a set of rules, or even to a degree a dialect of C#, within a single C# project. I control the compilation I can pick what analyzers I want to use.

Analyzers are less effective when there is a need to enforce rules across projects. In particular when those projects are owned by different people. There is no mechanism for enforcing that a given project reference was scanned by a particular analyzer. The only enforcement that exists is a hand shake agreement.

Immutable types is a feature though that requires cross project communication. The immutability of my type is predicated on the immutable of the type you control that I am embedding. If you break immutability in the next version you have broken my code. In my opinion hat kind of dependency is best done directly in the language.

from csharplang.

stephentoub avatar stephentoub commented on August 15, 2024

@MgSam, thanks, yes, I have seen it.

from csharplang.

jaredpar avatar jaredpar commented on August 15, 2024

@jods4

The only "risk" is to misuse a mutate block after an immutable class has been "published". I think that's acceptable (devs will always find their way to abuse, if only by reflection or unsafe code).

I disagree, that is precisely the problem that immutable types attempt to solve. They can be used without any context on how the type was created or care about who else has a reference to them. Once the possibility of mutations are introduced, even in a specified context, that guarantee goes away and they are just another mutating value.

The pattern you are describing here is valid but it more closely describes read only semantics vs. immutable.

from csharplang.

sharwell avatar sharwell commented on August 15, 2024

@jaredpar How would you have handled the StorageMetadata issue I described above? Edit: Not saying I disagree with you. This is simply an unsolved problem for me in the area of easy-to-use immutable types.

from csharplang.

jods4 avatar jods4 commented on August 15, 2024

@jaredpar
C# provides you memory safety, but you can shoot yourself in the foot inside an unsafe block.
To me, the mutate block is the same. Immutable are safe as long as you don't introduce a mutate block. Bonus: it makes it super-easy to construct your immutable objects, alleviating the need for lots of unwieldy APIs -- this is the usage mutate would be intended for and it is safe.

I also would like to point out that immutables will never be 100% safe in C#:

  • There are holes in the language "protection" of immutables, if only reflection.
  • You are proposing the unsafe modifier on immutable class, which is much more dangerous and hard to get right than the mutate block I proposed. (That said, as I described it the mutate block is not a replacement for unsafe immutable class, just saying the holes are already there.)

If you want immutable to be successful I think that you need to come up with a good solution for the construction problem (hint: the currently available T4 apis don't even come close).

The pattern you are describing here is valid but it more closely describes read only semantics vs. immutable.

I honestly don't understand why you say that.

from csharplang.

jaredpar avatar jaredpar commented on August 15, 2024

@sharwell

Essentially the problem of having easy ways to new instances of immutable values with different values for the fields?

This is a difficult nut to crack because of user defined constructors. They provide the guarantee of code that will execute for every single instance of a type. It's extremely useful for establishing invariants (the ImmutableArray<string> values will be non-empty). But this makes it really hard to provide a mechanism where the compiler generates helpers to change field values.

  • There is no way for the compiler to know for certain which constructor parameter relates to which field. Hence now way to generate the right constructor call in the helper.
  • Can't change the field values directly because that would be by passing any invariants established in the constructor.

I think it would be more feasible with features like records though or possibly primary constructors. It's not a slam dunk but those features could be made to work with generated helpers.

from csharplang.

jaredpar avatar jaredpar commented on August 15, 2024

@jods4

Immutable are safe as long as you don't introduce a mutate block.

And this is a design I simply don't agree with. The definition of immutable should need no qualifier, it is implicitly safe and requires no external verification or additional thought. I have a value and it won't change. Period.

C# provides you memory safety, but you can shoot yourself in the foot inside an unsafe block.

The unsafe keyword can do pretty much anything in C#. It can violate memory safety, type safety, mutate a string contents, etc ... It exists to facilitate low level code and places with high levels of interop. It should be treated as the dangerous item that it is, not as an excuse to reduce the safety in other features.

If you want immutable to be successful I think that you need to come up with a good solution for the construction problem

I agree that construction is a problem but I think it can be solved with existing patterns that don't reduce the deep guarantee provided by the current model. Essentially have simple constructors which mirror the field layout of the type. I've seen this approach successfully used on a very large code base with a high number of immutable types.

I honestly don't understand why you say that.

The design you are proposing essentially partitions object holders into two categories:

  • Holders which can mutate the object (those with a mutate block).
  • Holders which cannot mutate the object.

This pattern is much closer to how read only is used in the language and the BCL. For example:

  • ReadOnlyCollection<T>: This is a collection that can't be mutated by the holders of this reference but if you have a reference to the backing List<T> then it can be mutated.
  • ImmutableArray<T>: No one can mutate this no matter what reference they hold.

from csharplang.

jods4 avatar jods4 commented on August 15, 2024

Essentially have simple constructors which mirror the field layout of the type. I've seen this approach successfully used on a very large code base with a high number of immutable types.

I can come up with tons of examples where I've used 'immutable-like' types in the past that won't fit in:

  • Anything with a cycle.
  • Easiest way to change the "immutable" state of your app (I used immutable state to model the state of a game and have a parallell AI on top of it, it worked great): MemberwiseClone existing (potentially complex) state and then mutate a few members.
  • Loading some immutable reference data from xml files.
  • Even something as basic as efficiently handling large immutable arrays is a bit clumsy. Most existing libraries that could produce a large array (or matrix or anything built with an array inside, think scientific parallell computations) don't support ImmutableArray.Builder and hence incur a full array copy (even using ImmutableArray.Builder directly and hoping to avoid a copy is tricky).

Not saying my solution was ideal and we can try to come up with something different. But I think we need something better than a ctor with all properties as parameters. If this goes into the language it needs to be a good solution, I'd hate continuing to carefully use my own "immutable" classes because the safer built-in immutables are not easy enough to use.

Here's another idea I had... it's more complicated to implement and has one case that it doesn't handle: you proposed to introduce 'move' semantics into the language. Let's imagine that you can create a new mutable 'immutable' class (more or less as I described above), but that this mutable variable has a single ownership. Any copy has to be with move semantics. Calls to other methods would 'lend' the variable (ร  la Rust) but they wouldn't be able to store or capture it anywhere. On top of that, all its Reference fields have the same 'single ownership + move' semantics (transitively).
Once you are satisfied with your object, you do a special last 'move' that makes it immutable. Now there is no more single ownership, but the new variable is of immutable type.
As I said, much more complicated, but 100% safe and allows any mutation until 'publication' (that looks like a compiler-enforced Freezable). Only limitation that I see: you can only reference an immutable instance once in an immutable graph (because of the single ownership rule).

The design you are proposing essentially partitions object holders into two categories:

OK now I understand what you meant with readonly. My vision was that mutate would only be used for complex construction so I didn't see it in that "dual" way. Of course (like unsafe), devs could indeed do stupid thing and use it where they shouldn't, which would loose all safety that they may have. :(

As a closing thought, I would like to point out that the unsafe immutable class proposal will allow just the same dual world. Imagine that I want a very efficient ImmutableMatrix and that I'm thinking of simply taking an existing array in my ctor to avoid any copy cost. As I understand it the unsafe immutable class will allow me to do that. And I will then have those parts of the code that may still mutate the original array and those that have my ImmutableMatrix.

from csharplang.

MgSam avatar MgSam commented on August 15, 2024

I see a few problems:

  • The reliance on readonly fields. readonly is already a deeply flawed mechanism as it precludes using helper methods in the constructor. This would be especially problematic for the async object creation pattern:
public class Foo //This guy can never use the new immutable keyword
{
    private Data _data;
    private class Foo() { }
    public static async Task<Foo> Create(String bar) 
    {
         var foo = new Foo();
         foo._data = await someLongRunningOperation(bar);
         return foo;
    }
}
  • Immutable means different things in different contexts. The .NET/C# teams seem to take it as meaning snapshot immutability, but this is only one specific kind. I find the "cannot be mutated by anything external to the class" definition of immutability to be much more useful in the code I write everyday.
  • What happens when you need a copy of the object but with a different value in a field? Do you write the copy constructor yourself by hand? The with operator presents itself again.

from csharplang.

Clockwork-Muse avatar Clockwork-Muse commented on August 15, 2024

@MgSam - Is there something I'm unaware of about async, etc, or would passing the result into a single-arg constructor work? Would the compiler do dangerous things if I tried doing so?

from csharplang.

jods4 avatar jods4 commented on August 15, 2024

@MgSam

I find the "cannot be mutated by anything external to the class" definition of immutability to be much more useful in the code I write everyday.

One important use case for immutability is writing concurrent code. If the internal state of an "immutable" class changes then all thread-safety guarantees are lost, even if the changes are not observable from outside the class (e.g. self-optimizing search trees).

@Clockwork-Muse
A single arg that takes _data? That's OK but you'll soon have tons of args because real-world classes have a lot more than a single field. :(

from csharplang.

ashmind avatar ashmind commented on August 15, 2024

@sharwell

The fun part is generic type constraints.

Why can't you just apply the attribute to generic parameter itself? That's what I did with [ReadOnly] in https://github.com/ashmind/AgentHeisenbug.

from csharplang.

khellang avatar khellang commented on August 15, 2024

The string interpolation in the examples isn't valid syntax anymore.

public string FullName => "\{FirstName} \{LastName}";

should be changed to

public string FullName => $"{FirstName} {LastName}";

๐Ÿ˜„

from csharplang.

sharwell avatar sharwell commented on August 15, 2024

@ashmind In other words, a generic type Foo<T> marked [Immutable] could only include a field with type T if it were declared like this:

[Immutable]
class Foo<[Immutable] T1, T2>
{
    // allowed:
    private readonly T1 _value1;

    // compile-time error (field of immutable type is not readonly):
    private T1 _value2;

    // compile-time error (generic type parameter T2 is not immutable):
    private T2 _value3;
}

from csharplang.

sharwell avatar sharwell commented on August 15, 2024

Also, I would like to relax one of my previous rules:

A private field of an immutable type does not have to be marked readonly. However, the locations where the filed can be assigned is restricted by the compile-time analysis of immutable types. In particular:

  1. A field of an immutable type can include an initializer.

  2. A field of an immutable type can be assigned in the constructor.

  3. A field of an immutable type can be assigned prior to the point where the instance is "exposed". In the initial implementation this would likely have the following form:

    TypeName value = new TypeName(...); // or (TypeName)MemberwiseClone()
    value.field = ...; // allowed
    value.field2 = ...; // allowed
    value.Method();
    value.field3 = ...; // not allowed (value could have been exposed)
    return value;
    • Calling an instance method on the newly-constructed instance is considered "exposing" the instance, even if that method is marked [Pure].
    • With the exception of passing a value type by value, using the instance as an argument in a call is considered "exposing" the instance.
    • Returning the instance is considered exposing it.

from csharplang.

jods4 avatar jods4 commented on August 15, 2024

Maybe I'll state the obvious here, but I think that the problem of initializing an immutable graph has some overlap with the problem of initializing non-nullable references, which is another proposal under consideration. For instance, how could I create a graph of non-nullable references?
Figuring out a unified solution to both situations would be interesting.

from csharplang.

biqas avatar biqas commented on August 15, 2024

@pharring as stated in the #1125 issue, an immutable modifier is required. Readonly fields are not solving it.

Example:

public class B
{
    public int Value;
}

public class A
{
    private readonly B b;

    public B B { get { return this.b; } }

    public A()
    {
        this.b = new B();
    }
}

you can do following

var a = new A();
a.B.Value = 4;

but i was proposing that this should not be allowed. And to detect such modifications an extra immutable modifier is required.

public class A
{
    private immutable B b;

    public B B { get { return this.b; } }

    public A()
    {
        this.b = new B();
    }
}
var a = new A();
a.B.Value = 4; // compile error!!!

So it is not important a type it self is immutable, more important from which context it is used.

from csharplang.

cdauphinee avatar cdauphinee commented on August 15, 2024

I'm just curious, is there a reason you're proposing immutable classes, rather than a more general solution to C#'s entire category of problems involving immutability? For example, something akin to the C++ const qualifier.

from csharplang.

vladd avatar vladd commented on August 15, 2024

@cdauphinee ะก++'s const qualifier is indeed rather weak. It sort of guarantees read-only view on object (if everyone agrees not to use const_cast), but doesn't guarantee immutability (anyone may have a mutable view as well, and may pull the rug from under your feet and change the object while you think it's still the same). Immutable, on the other hand, implies reliable, true, genuine, compiler-guaranteed unchangeable objects.

from csharplang.

biqas avatar biqas commented on August 15, 2024

Here is what I had in mind when I thought about this feature.

1.Language
2.Threading
3.Garbage Collection (GC)
4.Security
5.Predictions

  1. Language
    Would require to have at least one additional keyword or more (immutable, immute).
    There must be some analysis regarding correct use of immutability, for the analysis
    I would recommend the project "Chess" http://research.microsoft.com/en-us/projects/chess/
    which could be a good starting point.
    There could be also some new constraints introduction. For constraints I think there is some
    ongoing discussion here, which need some CLR extension, but this is not needed in the first shot.
    But if talking about constraints maybe talking about modifier(s) is not applicable.
    And then there is an inheritance model for immutability. So currently I have not thought
    a lot about how inheritance model could work.
    Closure optimization for immutable type references.
  2. Threading
    To guaranty thread safety if using immutable types would depend on some analysis which
    then can postulate correctness.
    Optimization in parallel executions like local variables can be achieved.
    Locks can be optimized if all type references which are used in the lock scope could be
    transformed to immutable type references.
  3. GC
    Because of predictable memory consumption there could be some optimizations in memory allocations/reuse.
  4. Security
    Did not thought about if there could be potential security issues if the feature is deeply
    entrenched in the system.
  5. Predictions
    Because of the nature to be immutable someone could think of make behaviour also repeatable,
    in that case you could think of caching results for repeated access/invocations.

Taking that in mind I would like to make the proposal to have the immutability concept like that:

  1. By definition (partially/complete) | "immutable"
  2. By scope (explicit/implicit) | "immute" or "immutable"
  3. By Definition
    By definition is meant to decorate a modifier (immutable) on several nodes
    (class, struct, interface, field, property, event, indexer, method, namespace?) during the design phase.
    So if some node is caring such modifier(s) maybe it should be also inherit dependant nodes.
  4. By Scope
    To have the ability convert a non immutable type reference to an immutable one (immute).
    To achieve this, the visibility and type conversion aspects must be enlighten more in depth.

These are ruff ideas, what currently is missing, is to look more in detail how the data-flow would
be if introducing immutability, maybe some parts are redundant maybe some parts are missing.

#01
// Class with immutable modifier.
// To have a modifier here has a lot of implications.
public immutable class A
{
    #02
    // Because of type Result is not immutable, but should be used here as one,
    // some methods and properties must be in accessible, to guaranty immutability.
    // Maybe immutable modifier is redundant.
    // Maybe private set accessor is also redundant.
    public virtual immutable Result Result { get; private set; }

    #03
    public A(int @value = 0)
    {
        #04
        // Assignment like read-only fields.
        this.Result = new Result(@value);
    }

    #05
    // This method would not make lot of sense,
    // because is trying to store in instance backing store, but type A is immutable!
    public void Calculate1()
    {
        #16
        // Not valid, because immutable.
        this.Result = new Result(1);

        #07
        // Not valid, because entry reference is marked as immutable.
        this.Result.Value = 2;
    }

    #08
    // immutable modifier maybe here not needed.
    public A Calculate2()
    {
        #09
        return new A(42);
    }

    #10
    // Here are lot of questions, what should happen if you immute type B?!
    // Maybe should be restricted.
    public B Calculate3()
    {
        #11
        return new B(42);
    }

    #12
    public immutable Result GetResult1()
    {
        #13
        return this.Result;
    }

    #14
    // Because the return type is not marked as immutable,
    // the new reference is loosing the ability.
    public Result GetResult2()
    {
        #15
        return this.Result;
    }

    #16
    public immutable Result GetResult3()
    {
        #17
        // Not sure if implicitly an immutable reference
        // is created or should lead to an error.
        return new Result(1);

        // immute return new Result(1);
    }
}

#01
public class B : A
{
    #02
    // Not thought a lot about that.
    public override immutable Result Result { get; set; }

    #02
    public B(int @value = 0) : base(@value) {}

    #03
    public Reset()
    {
        #04
        // Not thought a lot about that.
        this.Result = new Result(0);
    }
}

#01
public class Result
{
    #02
    private int _value;

    #03
    public int Value
    {
        #04
        get { return this._value; }
        #05
        set { this._value = value; }
    }

    #06
    public Result(int @value)
    {
        #07
        this.Value = @value;
    }

    #08
    public void Reset()
    {
        #09
        this.Value = 0;
    }
}



#01
// Not thought a lot about that.
public immutable class Result2
    #02
    : Result
{
    #03
    public Result2(int @value)
        #04
        : base(@value)
    {
        #05
        this.Value = value;
    }
}

// Not thought a lot about that.
public class Generic<T> where T: immutable {}
#01
var a = new A();

#02
// Not thought a lot about that.
// Give immutability to reference if possible.
var b = immute a.Calculate3();

from csharplang.

GSPP avatar GSPP commented on August 15, 2024

If immutable types are defined to have no identity (meaning that they cannot be reference compared and hashing is based on value) then the CLR is free to allocate them like structs or like classes. However it sees fit. The difference would not be detectable. It can even use a mixed model. This would be subject to an optimization policy.

from csharplang.

alrz avatar alrz commented on August 15, 2024

How about a immutable list just like FSharpList<>? then it can support head::tail pattern, head::tail (cons operator) and list@list (list concatenation operator).

from csharplang.

Clockwork-Muse avatar Clockwork-Muse commented on August 15, 2024

@alrz - most of this discussion has been about the underlying mechanisms. You could write a readonly-based implementation today that would need minimal conversion work later (I would be completely unsurprised if one already exists, though).

from csharplang.

jviau avatar jviau commented on August 15, 2024

If C# implements immutable types, I would like to see language level support for building and transforming immutables (return a new object with the desired changes). Currently this is achieved with either the builder pattern or WithXXX pattern. Each with their pros and cons.

Builder

Pros

  1. Can to add new properties and methods and not break code compiled against older binaries.
  2. Allows you to perform batch changes without creating several immutable copies.

Cons

  1. Still is creating 1 (or more) extra object(s), which can be a noticable impact on the GC if you are modifying a large structure, like an immutable tree.
  2. Potentially verbose depending on the builder implementation (mutate via chainable methods vs setting properties)
    • Create builder, change each property one by one, convert to immutable.
  3. Requires maintaining another class.
    • For best builder experience you also want to ensure nested objects have builders as well.

WithXXX

Pros

  1. Allows changing of a property in one call (which returns a new object).
  2. Default properties can be used to create a single With method that allows changing of all properties in a single efficient call.

Cons

  1. A lot of methods to maintain
  2. No batch property changes when using WithXXX
    • Not the case with default properties
  3. Adding proprties and then changing the signature of the With method will break code compiled against previous versions

Language Support

As you can see both of these methods work, but come with downfalls. My suggestion is to solve this with something similar C# object initializer.

immutable class Foo
{
    string Bar;
    int Bazz;
}

// Initialize Foo
var foo = new Foo { Bar = "bar", Bazz = 10 }; // properties of Foo cannot be changed after this point

// Transform Foo, instantiating a new version of Foo with whatever fields 
// specified and then the rest of the values taken from foo
var foo2 = foo { Bar = "changed" };

foo.Bar == "bar"; // true 
foo2.Bar == "changed"; // true
foo.Bazz == 10; //true
foo2.Bazz == 10; // true

This provides us with the best of Builder and WithXXX.

  • Transformations can be done in bulk
  • No extra objects created to be later garbage collected
  • Adding properties will not break already compiled code targeting an old version

from csharplang.

gafter avatar gafter commented on August 15, 2024

@jviau See #5172.

from csharplang.

alrz avatar alrz commented on August 15, 2024

@AlexRadch check out #7626 for immutable variables.

from csharplang.

jpierson avatar jpierson commented on August 15, 2024

I think there is overlap with the discussion of a pure keyword. In that discussion I mentioned the idea being able to hoist the pure keyword to the class level in order to essentially allow constraining a class implementation to only pure members. In this approach a pure class would be equivalent to an immutable class because both imply that there is no mutation that can be done by it`s members.

The concept of deep immutability is more important than naming but I do like the way that the word pure more intuitively applies to both the class and class member levels.

Additionally I proposed the idea of an isolated keyword which would be less strict than pure in that it would allow mutation only of state locally owned by that class (ex. fields).

from csharplang.

shaggygi avatar shaggygi commented on August 15, 2024

Any updates on this proposal? I recall immutable related features coming (or at least mentioned in Roslyn demonstrations) to C#. Is this topic something being looked at to make it in C# 8? Just curious.

from csharplang.

shaggygi avatar shaggygi commented on August 15, 2024

@stephentoub @MadsTorgersen I'm assuming this would be a topic that would move to the new C# Language repo. If so, what would be the steps to take? Thx

from csharplang.

dmitriyse avatar dmitriyse commented on August 15, 2024

Immutability annotation can be applied also to a method arguments. For example if argument is R/W collection, then

public void MyFunc(immutable ICollection<object> t)
{
    t.Add(new object()); //Compile error/warning.
}

immutable keywork will inform developer and compiler that R/W collection will never change
So it's sort of static code analysis in the compile time.
The same proposal as for not-nullable referenced types.
See also
#219 (comment)

from csharplang.

soroshsabz avatar soroshsabz commented on August 15, 2024

I think some of this discussion go ahead in #115.

from csharplang.

Neme12 avatar Neme12 commented on August 15, 2024

@sanisoclem Please move this to dotnet/csharplang

from csharplang.

jpierson avatar jpierson commented on August 15, 2024

Possibly related to #776?

from csharplang.

crandsvrt avatar crandsvrt commented on August 15, 2024

I'm keen for this feature.

From an architectural perspective, when i receive an object from an API/interface I would ideally, optionally, like to have an immutable constraint, meaning that there is nothing the consumer can do to update that object without going through the interface again.

The primary drive for this is that, if a developer returns a non-immutable object from the interface, this can effectively serve as a back door into the originating module - circumnavigating the interface - which may not always be desirable.

Having types with deep immutability would ensure strict enforcement of the interface (no back doors), which will ultimately ensure there are fewer surprises when rearchitecting the way the module is used - for example, if i want to move my module so it is now exposed through a web service (as any back doors into returned objects are not an option).

Having this constraint ensures that we can identify risks at compile-time, to prevent any mistakes in development, and ensure interface-consistency regardless of application.

As discussed above, the would mean deep immutability - types that satisfy the immutable constraint can only contain other immutables, and all functions are pure.

from csharplang.

Shadowblitz16 avatar Shadowblitz16 commented on August 15, 2024

This is C# right?
why not just follow the way c does it and do const instead of immutable?
Its also shorter.
So something like a class would be..

public const class A
{
    public int Property {get;}
}
public class B : A
{
    public int Property {get; set;}
}
var b = new B();
var a = (A)b;

according to google in c++...

ReadOnly Vs Const Keyword
ReadOnly is a runtime constant. Const is a compile time constant. The value of readonly field can be changed. The value of the const field can not be changed.

from csharplang.

333fred avatar 333fred commented on August 15, 2024

Because, as your definition says, const is a compile time constant. This issue is not about compile time constants.

from csharplang.

Shadowblitz16 avatar Shadowblitz16 commented on August 15, 2024

its about immutability no? a compile time constant is immutable.

from csharplang.

HaloFour avatar HaloFour commented on August 15, 2024

a compile time constant is immutable.

But being immutable doesn't make it a compile time constant. These types can be evaluated/initialized at runtime and still be immutable, so this proposal is not related to compile time constants.

from csharplang.

HaloFour avatar HaloFour commented on August 15, 2024

@dsinghvi

A bit adjacent, I'm wondering if there is a C# equivalent for https://immutables.github.io/ ?

C# has source generators, which can be combined with partial types to emit the boilerplate code for such a type.

from csharplang.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.