Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Emulating F# `with` keyword in C#

Tags:

c#

f#

Is there a way to emulate F#'s with keyword in C#? I know it will likely not be as elegant, but I'd like to know if there's any way to handle creating new immutable copies of data structures.

Records in F# are detailed here.

Here's an example of what I'm trying to do. We'll create "immutable" views of data via interfaces, while maintaining mutability in concrete classes. This lets us mutate locally (while working) and then return an immutable interface. This is what we're handling immutability in C#.

public interface IThing
{
    double A { get; }
    double B { get; }
}

public class Thing : IThing
{
    double A { get; set; }
    double B { get; set; }
}

However, when it comes time to make a change to the data, it's not very type (or mutability!) safe to cast it back and forth, and it's also a real pain to manually translate each property of the class into a new instance. What if we add a new one? Do I have to go track down each manipulation? I don't want to create future headache when I really only need what I had before, but with [some change].

Example:

// ...

IThing item = MethodThatDoesWork();

// Now I want to change it... how? This is ugly and error/change prone:
IThing changed = new Thing {
    A = item.A,
    B = 1.5
};

// ...

What are sound strategies for accomplishing this? What have you used in the past?

like image 655
jocull Avatar asked Aug 24 '15 04:08

jocull


2 Answers

As there is no syntactic sugar I am aware of you'll have to either:

  • do it by hand (see below)
  • use some reflection/automapper (not a fan of this)
  • use some AOP techniques (neither a fan of those)

At least this is what I can think of right now.

I don't think the last two are a good idea because you bring on the big machinery to solve a very easy problem.

Yes when you have thousands of data-structures you might rethink this, but if you only have a couple of them I would not use it.

So what's left is basically smart-constructors and stuff like this - here is a simple example of how you could do it (note that you don't really need all of this - pick and choose) - it's basically missusing null/nullable to look for what you need - better options to this might be overloads or something like an Option<T> data-type but for now I think you get it:

class MyData
{
    private readonly int _intField;
    private readonly string _stringField;

    public MyData(int intField, string stringField)
    {
        _intField = intField;
        _stringField = stringField;
    }

    public MyData With(int? intValue = null, string stringValue = null)
    {
        return new MyData(
            intValue ?? _intField,
            stringValue ?? _stringField);
    }

    // should obviously be put into an extension-class of some sort
    public static MyData With(/*this*/ MyData from, int? intValue = null, string stringValue = null)
    {
        return from.With(intValue, stringValue);
    }

    public int IntField
    {
        get { return _intField; }
    }

    public string StringField
    {
        get { return _stringField; }
    }
}
like image 88
Random Dev Avatar answered Sep 24 '22 23:09

Random Dev


To add to Carsten's correct answer, there's no way to do this in C# because it's not in the language. In F#, it's a language feature, where succinct record declaration syntax expands to quite a bit of IL. C# doesn't have that language feature (yet).

This is one of the reasons I no longer like to work in C#, because there's too much overhead compared to doing the same thing in F#. Still, sometimes I have to work in C# for one reason or the other, and when that happens, I bite the bullet and write the records by hand.

As an example, the entire AtomEventSource library is written in C#, but with immutable records. Here's an abbreviated example of the AtomLink class:

public class AtomLink : IXmlWritable
{
    private readonly string rel;
    private readonly Uri href;

    public AtomLink(string rel, Uri href)
    {
        if (rel == null)
            throw new ArgumentNullException("rel");
        if (href == null)
            throw new ArgumentNullException("href");

        this.rel = rel;
        this.href = href;
    }

    public string Rel
    {
        get { return this.rel; }
    }

    public Uri Href
    {
        get { return this.href; }
    }

    public AtomLink WithRel(string newRel)
    {
        return new AtomLink(newRel, this.href);
    }

    public AtomLink WithHref(Uri newHref)
    {
        return new AtomLink(this.rel, newHref);
    }

    public override bool Equals(object obj)
    {
        var other = obj as AtomLink;
        if (other != null)
            return object.Equals(this.rel, other.rel)
                && object.Equals(this.href, other.href);

        return base.Equals(obj);
    }

    public override int GetHashCode()
    {
        return
            this.Rel.GetHashCode() ^
            this.Href.GetHashCode();
    }

    // Additional members removed for clarity.
}

Apart from the overhead of having to type all of this, it's also been bothering me that if you're doing (dogmatic) Test-Driven Development (which you don't have to), you'd want to test these methods as well.

Using tools like AutoFixture and SemanticComparison, though, you can make it somewhat declarative. Here's an example from AtomLinkTests:

[Theory, AutoAtomData]
public void WithRelReturnsCorrectResult(
    AtomLink sut,
    string newRel)
{
    AtomLink actual = sut.WithRel(newRel);

    var expected = sut.AsSource().OfLikeness<AtomLink>()
        .With(x => x.Rel).EqualsWhen(
            (s, d) => object.Equals(newRel, d.Rel));
    expected.ShouldEqual(actual);
}

Here, it's still relatively verbose, but you can easily refactor this to a generic method, so that each test case becomes a one-liner.

It's still a bother, so even if you're writing most of your code in C#, you might consider defining your immutable types in a separate F# library. Viewed from C#, F# records look like 'normal' immutable classes like AtomLink above. Contrary to some other F# types like discriminated unions, F# records are perfectly consumable from C#.

like image 24
Mark Seemann Avatar answered Sep 23 '22 23:09

Mark Seemann