Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can we get access to the F# copy and update feature from c#?

For example in F# we can define

type MyRecord = {
    X: int;
    Y: int;
    Z: int 
    }

let myRecord1 = { X = 1; Y = 2; Z = 3; }

and to update it I can do

let myRecord2 = { myRecord1 with Y = 100; Z = 2 }

That's brilliant and the fact that records automatically implement IStructuralEquality with no extra effort makes me wish for this in C#. However Perhaps I can define my records in F# but still be able to perform some updates in C#. I imagine an API like

MyRecord myRecord2 = myRecord
    .CopyAndUpdate(p=>p.Y, 10)
    .CopyAndUpdate(p=>p.Z, 2)

Is there a way, and I don't mind dirty hacks, to implement CopyAndUpdate as above? The C# signiture for CopyAndUpdate would be

T CopyAndUpdate<T,P>
   ( this T
   , Expression<Func<T,P>> selector
   , P value
   )
like image 251
bradgonesurfing Avatar asked Aug 09 '13 17:08

bradgonesurfing


Video Answer


2 Answers

It can be done, but doing that properly is going to be quite hard (and it definitely won't fit in my answer). The following simple implementation assumes that your object has only read-write properties and parameter-less constructor:

class Person
{
  public string Name { get; set; }
  public int Age { get; set; }
}

This slightly defeats the point, because you would probably want to use this on immutable types - but then you always have to call the constructor with all the arguments and it is not clear how to link the constructor parameters (when you create an instance) with the properties that you can read.

The With method creates a new instance, copies all property values and then sets the one that you want to change (using the PropertyInfo extracted from the expression tree - without any checking!)

public static T With<T, P>(this T self, Expression<Func<T, P>> selector, P newValue)
{
  var me = (MemberExpression)selector.Body;
  var changedProp = (System.Reflection.PropertyInfo)me.Member;

  var clone = Activator.CreateInstance<T>();
  foreach (var prop in typeof(T).GetProperties())
    prop.SetValue(clone, prop.GetValue(self));

  changedProp.SetValue(clone, newValue);
  return clone;
}

The following demo behaves as expected, but as I said, it has lots of limitations:

var person = new Person() { Name = "Tomas", Age = 1 };
var newPerson = person.With(p => p.Age, 20);

In general, I think using a universal reflection-based method like With here might not be such a good idea, unless you have lots of time to implement it properly. It might be easier to just implement one With method for every type that you use which takes optional parameters and sets their values to a cloned value (created by hand) if the value is not null. The signature would be something like:

public Person With(string name=null, int? age=null) { ... }
like image 71
Tomas Petricek Avatar answered Oct 16 '22 18:10

Tomas Petricek


You could achieve something similar using optional arguments:

class MyRecord {
    public readonly int X;
    public readonly int Y;
    public readonly int Z;
    public MyRecord(int x, int y, int z) {
        X = x; Y = y; Z = z;
    }
    public MyRecord(MyRecord prototype, int? x = null, int? y = null, int? z = null)
        : this(x ?? prototype.X, y ?? prototype.Y, z ?? prototype.Z) { }
}

var rec1 = new MyRecord(1, 2, 3);
var rec2 = new MyRecord(rec1, y: 100, z: 2);

This is actually pretty close to the code that F# generates for records.

like image 32
Daniel Avatar answered Oct 16 '22 18:10

Daniel