Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

C# Immutable class sub class

I have a type that is an immutable type, and I'd like to create a subclass of it that has access to all the same methods.

However, because of how you have to implement an immutable class, the base class methods return my parent type, not my child type. Is it possible to create an immutable class that can have sub classes that return the subclass?

Below is example code to run in LinqPad that demonstrates the problem

void Main()
{
    var immutable = new MyImmutable(new Dictionary<ImmutableKey, decimal>{
        { ImmutableKey.Key1, 1 },
        { ImmutableKey.Key2, -5 },
        { ImmutableKey.Key3, 1.25m },
    });
    
    var immutable2 = new MyImmutable(new Dictionary<ImmutableKey, decimal>{
        { ImmutableKey.Key1, 1 },
        { ImmutableKey.Key2, 2 },
        { ImmutableKey.Key3, 3 },
    });
    
    var added = immutable.Apply((a, b) => a + b, immutable2);
    added[ImmutableKey.Key1].Dump();
    added[ImmutableKey.Key2].Dump();
    added[ImmutableKey.Key3].Dump();
    
    var subImmutable1 = new SubImmutable(1, new Dictionary<ImmutableKey, decimal>{
        { ImmutableKey.Key1, 1 },
        { ImmutableKey.Key2, -5 },
        { ImmutableKey.Key3, 1.25m },
    });
    var subImmutable2 = new SubImmutable(1, new Dictionary<ImmutableKey, decimal>{
        { ImmutableKey.Key1, 1 },
        { ImmutableKey.Key2, 2 },
        { ImmutableKey.Key3, 3 },
    });
    
    var subImmutableAdded = subImmutable1.Apply((a, b) => a + b, subImmutable2);
    subImmutableAdded.GetType().Name.Dump(); //prints MyImmutable, it's not a SubImmutable
    //after adding two SubImmutables, the type is changed back to the base type
    
    var asSub = (SubImmutable)subImmutableAdded; // Unable to cast object of type 'MyImmutable' to type 'SubImmutable', SomeOtherValue was lost.
}

public enum ImmutableKey 
{
    Key1,
    Key2,
    Key3
}

public class MyImmutable
{
    protected static readonly IEnumerable<ImmutableKey> AllKeys = Enum.GetValues(typeof(ImmutableKey)).Cast<ImmutableKey>();
    
    private Dictionary<ImmutableKey, decimal> _dict { get; set; }
    
    public MyImmutable(Dictionary<ImmutableKey,decimal> d)
    {
        _dict = d;
    }
    
    public decimal this[ImmutableKey key]
    {
        get
        {
        if (_dict == null || !_dict.ContainsKey(key))
            return 0;

        return _dict[key];
        }
    }
    
    public MyImmutable Apply(Func<decimal, decimal, decimal> aggFunc, MyImmutable y)
    {
        var aggregated = new Dictionary<ImmutableKey, decimal>(AllKeys.Count());
        foreach (ImmutableKey bt in AllKeys)
        {
            aggregated[bt] = aggFunc(this[bt], y[bt]);
        }
        return new MyImmutable(aggregated);
    }
}

public class SubImmutable : MyImmutable
{
    public int SomeOtherValue { get; set; }
    public SubImmutable(int someValue, Dictionary<ImmutableKey,decimal> d)
        :base(d)
    {
        SomeOtherValue= someValue;
    }
}

Output:

2

-3

4.25

MyImmutable

InvalidCastException: Unable to cast object of type 'MyImmutable' to type 'SubImmutable'.

Is there a way I can have an inherited immutable type that can inherit all the methods in the base type without having to reimplement them all?

Companion CodeReview question: https://codereview.stackexchange.com/questions/79380/inheriting-methods-of-an-immutable-type

like image 311
DLeh Avatar asked Oct 19 '22 17:10

DLeh


2 Answers

One of the main issues with combining immutability and inheritance is that you want operations like your Apply to accept and return instances of the derived class it is called on, not the base class

That is you want MyImmutable.Apply to be:
public MyImmutable Apply(Func<decimal, decimal, decimal> aggFunc, MyImmutable y)

and SubImmutable.Apply to be:
public SubImmutable Apply(Func<decimal, decimal, decimal> aggFunc, SubImmutable y)

You can neatly solve this by creating an abstract base class that all concrete classes (MyImmutable and SubImmutable) derive from using the 'curiously recurring template pattern'

See below, I also changed your code a bit to my taste :) note that Dict here is not readonly so the classes are publicly (and so effectively) immutable but internally mutable.

public enum ImmutableKey { Key1, Key2, Key3 }

abstract class MyImmutableBase<TDerived> where TDerived : MyImmutableBase<TDerived> {
  protected static readonly IEnumerable<ImmutableKey> AllKeys = Enum.GetValues(typeof(ImmutableKey)).Cast<ImmutableKey>();
  private ImmutableDictionary<ImmutableKey, decimal> Dict;

  public MyImmutableBase() => Dict = ImmutableDictionary<ImmutableKey, decimal>.Empty;

  protected abstract TDerived GetNew();

  public decimal this[ImmutableKey key] { get { if (Dict == null || !Dict.ContainsKey(key)) return 0; return Dict[key]; } }

  public TDerived Add(IEnumerable<KeyValuePair<ImmutableKey, decimal>> d) {
    var res = GetNew();
    res.Dict = res.Dict.AddRange(d);
    return res;
  }

  public TDerived Apply(Func<decimal, decimal, decimal> aggFunc, TDerived y) {
    var aggregated = ImmutableDictionary<ImmutableKey, decimal>.Empty;
    foreach (ImmutableKey bt in AllKeys) aggregated = aggregated.SetItem(bt, aggFunc(this[bt], y[bt]));
    return GetNew().Add(aggregated);
  }
}


class MyImmutable : MyImmutableBase<MyImmutable> {
  protected override MyImmutable GetNew() => new();
}

class SubImmutable : MyImmutableBase<SubImmutable> {
  public int SomeOtherValue { get; init; }
  public SubImmutable(int someValue) : base() => SomeOtherValue = someValue;
  protected override SubImmutable GetNew() => new(SomeOtherValue);
}
like image 25
kofifus Avatar answered Oct 27 '22 01:10

kofifus


You can use a virtual method to get the new instance.

Make a virtual method in the base class that takes the input to create a new instance of the base class and returns a new instance of the base class. Then override it in the subclass to generate any additional inputs the subclass needs and return a new instance of the subclass.

public class MyImmutable
{
    // other stuff

    // add this method
    protected virtual MyImmutable GetNew(Dictionary<ImmutableKey, decimal> d)
    {
        return new MyImmutable(d);
    }

    // modify this method as shown
    public MyImmutable Apply(Func<decimal, decimal, decimal> aggFunc, MyImmutable y)
    {
        var aggregated = new Dictionary<ImmutableKey, decimal>(AllKeys.Count());
        foreach (ImmutableKey bt in AllKeys)
        {
            aggregated[bt] = aggFunc(this[bt], y[bt]);
        }
        return GetNew(aggregated);
    }
}

public class SubImmutable : MyImmutable
{
    // other stuff

    // add this method
    protected override MyImmutable GetNew(Dictionary<ImmutableKey, decimal> d)
    {
        return new SubImmutable(SomeOtherValue, d);
    }
}

This way any transformations that don't care about the subclass' extra stuff don't need to be overridden in the subclass.

Some transformations may still need to be overridden. For example:

var one = new SubImmutable(1, alpha);
var two = new SubImmutable(2, alpha);
var test1 = one.Apply((a, b) => a + b, two);
var test2 = two.Apply((a, b) => a + b, one);
Console.WriteLine(test1[someKey] == test2[someKey]); // true
Console.WriteLine(test1.SomeOtherValue == test2.SomeOtherValue); // false

If you want test1 and test2 to have the same SomeOtherValue, then you'll have to make the Apply method virtual and then override it in the subclass.

like image 74
Oblivious Sage Avatar answered Oct 26 '22 23:10

Oblivious Sage