-
Notifications
You must be signed in to change notification settings - Fork 15
CopyWith
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
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.
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.
Action
/Func
conversionsCycle
methods- Converting between
Action
andFunc
- Extension methods for existing types that use
Option<T>
- Indexed enumerations
IEnumerable<T>
cons- Option-based parsers
- Partial function applications
- Pattern matching
- Pipe Operators
- Typed lambdas
Any
Either<TLeft,TRight>
None
Option<T>
Success<T>
Union<T1,T2>
Union<T1,T2,T3>
Union<T1,T2,T3,T4>
Unit
ValueOrError