Skip to content
David Arno edited this page Feb 17, 2020 · 1 revision

Copy/With functions


When defining a type, there is one important decision that has to be made: can its value/state change? If we make it mutable, we introduce challenges around complexity and tracking down bugs when values unexpectedly change. But it offers flexibility too: an app with zero internal change is rarely a useful app; the real world mutates and if we are to model that, our app's state must mutate too. Immutable types might be less flexible when it comes to modelling change, but they bring simplicity to the code. If a value can't change, it can't unexpectedly change and thus a whole category of bugs is removed.

Functional languages deal with this dichotomy - flexibility versus reliability - through the use of "withers" (F# uses the term, "copy-and-update record expression", but "withers" is shorter at least):

let london = { lat=51.5, long=0.13 }
let brighton = london with { lat=50.82 }

Rather than modifying london, we create a new location from it, using the notation ... with { changing values }, to create the new location, brighton.

C# doesn't (yet) have built-in support for withers, though at the time of writing, it's being considered for C# 9. In the meantime, Succinc<T> supplies its own way of copying and "updating" immutable structs and objects via a set of Copy and With functions.

These functions work on the basis that any type they are used with are immutable types and that there must exist a constructor for each type that permits the initialisation of all the properties of that type, or that all properties that aren't covered by a constructor parameter are themselves writable.

So trying to use these functions with the following type would fail at runtime as C in read-only and has a different name to the d constructor parameter and so can't be matched:

class C1
{
    C1(int a, int d) => (A, C) = (a, d);

    int A { get; }
    int B { get; set; }
    int C { get; }
}

The following type is "just right" for this feature and can be used with the copy/with functions:

class C2
{
    C2(int a, int b) => (A, B) = (a, b);

    int A { get; }
    int B { get; }
    int C { get; set; }
}

var a = new C2(1, 2); // a == 1, 2, 0
var b = a.Copy(); // b == 1, 2, 0
var c = a.With(new { C = 3 }); // c == 1, 2, 3

Copy functions

The Copy()/TryCopy() functions provide a way to create a direct copy of an existing object.

var firstCar = new Car {
    Constructor = "Ford", 
    Color = "Black", 
    CreationDate = new DateTime(1908, 10, 1)
};

var secondCar = firstCar.Copy();
var thirdCardOption = firstCar.TryCopy();

public static T Copy<T>(this T @object) where T : notnull
This function creates a copy of @object. If you have the nullable reference types feature enabled with C# 8, it'll emit a compiler warning if you try to use it with a null value for @object. Otherwise it'll throw a CopyException at runtime. Likewise, should anything go wrong with the copy (such as using it with C1 above), it'll throw a CopyException.

public static Option<T> TryCopy<T>(this T @object) where T : notnull
This version of the function will return an Option containing some(T) (a copy of @object) if all is well and none should anything go wrong with the copy.

With functions

The With()/TryWith() provides a way to create a new object by copying properties of an existing object and updating some properties in the object (ie a "wither").

var firstCar = new Car {
    Constructor = "Ford", 
    Color = "Black", 
    CreationDate = new DateTime(1908, 10, 1)
};

var secondCar = firstCar.With(new { Color = "Red" });
var thirdCardOption = firstCar.TryWith(new { Color = "Red" });

public static Option<T> TryWith<T, TProps>(this T itemToCopy, TProps propertiesToUpdate)
where T : notnull where TProps : class
This function creates a new object of type T. For each of the properties in the object, propertiesToUpdate, the properties of the new object are set to those values. For all other properties, the values from itemToCopy are used.

If you have the nullable reference types feature enabled with C# 8, it'll emit a compiler warning if you try to use it with a null value for itemToCopy. Otherwise it'll throw a CopyException at runtime. Likewise, should anything go wrong with the wither (such as using it with C1 above), it'll throw a CopyException.

public static Option<T> TryWith<T, TProps>(this T itemToCopy, TProps propertiesToUpdate)
where T : notnull where TProps : class
This version of the function will return an Option containing some(T) (a new object created via the rules of the wither) if all is well and none should anything go wrong with the creation.