Releases: louthy/language-ext
Migrate to `net461` and `netstandard2.1`
NOTE: I am just investigating some issues with this release relating to the code-gen, keep an eye out for 3.4.3 tonight or tomorrow (12/Feb/2020)
In an effort to slowly get language-ext to the point where .NET Core 3 can be fully supported (with all of the benefits of new C# functionality) I have taken some baby steps towards that world:
Updated the references for CodeGeneration.Roslyn
to 0.7.5-alpha
This might seem crazy, but the CodeGeneration.Roslyn
DLL doesn't end up in your final build (if you set it up correctly), and doesn't get used live even if you do. So, if the code generates correctly at build-time, it works. Therefore, including an alpha
is low risk.
I have been testing this with my TestBed and unit-tests and working with the CodeGeneration.Roslyn
team and the alpha
seems stable.
A release of CodeGeneration.Roslyn
is apparently imminent, so, if you're not happy with this, then please wait for subsequent releases of language-ext when I've upgraded to the full CodeGeneration.Roslyn
release. I just couldn't justify the code-gen holding back the development of the rest of language-ext any more.
Updated the minimum .NET Framework and .NET Standard versions
Ecosystem | Old | New |
---|---|---|
.NET Framework | net46 |
net461 |
.NET Standard | netstandard2.0 |
netstandard2.1 |
OptionAsync<A>
and EitherAsync<A>
support IAsyncEnumerable<A>
The netstandard2.1
release supports IAsyncEnumerable<A>
for OptionAsync<A>
and EitherAsync<A>
. This is the first baby-step towards leveraging some of the newer features of C# and .NET Core.
pipe
prelude function
Allow composition of single argument functions which are then applied to the initial argument.
var split = fun((string s) => s.Split(' '));
var reverse = fun((string[] words) => words.Rev().ToArray());
var join = fun((string[] words) => string.Join(" ", words));
var r = pipe("April is the cruellest month", split, reverse, join); //"month cruellest this is April"
Added Hashable<A>
and HashableAsync<A>
type-classes
Hashable<A>
and HashableAsync<A>
provide the methods GetHashCode(A x)
and GetHashCodeAsync(A x)
. There are lots of Hashable*<A>
class-instances that provide default implementations for common types.
Updates to the [Record]
and [Union]
code-gen
The GetHashCode()
code-gen now uses Hashable*<A>
for default field hashing. Previously this looked for Eq*<A>
where the *
was the type of the field to hash, now it looks for Hashable*<A>
.
By default Equals
, CompareTo
, and GetHashCode
use:
// * == the type-name of the field/property
default(Eq*).Equals(x, y);
default(Ord*).CompareTo(x, y);
default(Hashable*).GetHashCode(x);
To provide the default structural functionality for the fields/properties. Those can now be overridden with The Eq
, Ord
, and Hashable
attributes:
[Record]
public partial struct Person
{
[Eq(typeof(EqStringOrdinalIgnoreCase))]
[Ord(typeof(OrdStringOrdinalIgnoreCase))]
[Hashable(typeof(HashableStringOrdinalIgnoreCase))]
public readonly string Forename;
[Eq(typeof(EqStringOrdinalIgnoreCase))]
[Ord(typeof(OrdStringOrdinalIgnoreCase))]
[Hashable(typeof(HashableStringOrdinalIgnoreCase))]
public readonly string Surname;
}
The code above will generate a record where the fields Forename
and Surname
are all structurally part of the equality, ordering, and hashing. However, the case of the strings is ignored, so:
{ Forename: "Paul", Surname: "Louth" } == { Forename: "paul", Surname: "louth" }
NOTE: Generic arguments aren't allowed in attributes, so this technique is limited to concrete-types only. A future system for choosing the structural behaviour of generic fields/properties is yet to be designed/defined.
Bug fixes
`Non*` attributes respected on `[Record]` and `[Union]` types
The attributes:
NonEq
- to opt out of equalityNonOrd
- to opt out of orderingNonShow
- to opt out ofToString
NonHash
- to opt out ofGetHashCode
NonSerializable
,NonSerialized
- to opt out of serialisationNonStructural == NonEq | NonOrd | NonHash
NonRecord == NonStructural | NonShow | NonSerializable
Can now be used with the [Record]
and [Union]
code-gen.
For [Union]
types you must put the attributes with the arguments:
[Union]
public abstract partial class Shape<NumA, A> where NumA : struct, Num<A>
{
public abstract Shape<NumA, A> Rectangle(A width, A length, [NonRecord] A area);
public abstract Shape<NumA, A> Circle(A radius);
public abstract Shape<NumA, A> Prism(A width, A height);
}
On the [Record]
types you put them above the fields/properties as normal:
[Record]
public partial struct Person
{
[NonOrd]
public readonly string Forename;
public readonly string Surname;
}
Both the [Union]
case-types and the [Record]
types now have a New
static function which can be used to construct a new object of the respective type. This can be useful when trying to construct types point-free.
Some minor bug fixes to Try.Filter
and manyn
in Parsec. Thanks to @bender2k14 and @StefanBertels
Important update: Fix for performance issue in `Lst<A>`
A bug had crept into the Lst<A>
type which would cause a complete rebuild of the data-structure when performing a transformation operation (like Add(x)
). This was caught whilst building benchmarks for comparisons with Seq<A>
and the .NET ImmutableList<T>
type.
The performance gets exponentially worse as more items are added to the collection, and so if you're using Lst<A>
for anything at all then it's advised that you get this update.
Luckily, there are now benchmarks in the LanguageExt.Benchmarks project that will pick up issues like these if they arise again in the future.
Collection `ToString` and various fixes
Collections ToString
All of the collection types now have a default ToString()
implementation for small list-like collections:
"[1, 2, 3, 4, 5]"
And for maps: (HashMap
and Map
):
"[(A: 1), (B: 2), (C: 3), (D: 4), (E: 5)]"
Larger collections will have CollectionFormat.MaxShortItems
and then an ellipsis followed by the number of items remaining. Unless the collection is lazy, in which case only the ellipsis will be shown:
"[1, 2, 3, 4, 5 ... 50 more]"
CollectionFormat.MaxShortItems
can be set directly if the default of 50
items in a ToString()
isn't suitable for your application.
In addition to this there's two extra methods per collection type:
string ToFullString(string separator = ", ")
This will build a string from all of the items in the collection.
string ToFullArrayString(string separator = ", ")
This will build a string from all of the items in the collection and wrap with brackets [ ]
.
Fixes
HashMap and Map equality consistency
HashMap
and Map
had inconsistent equality operators. HashMap
would compare keys and values and Map
would compare keys only. I have now unified the default equality behaviour to keys and values. This may have breaking changes for your uses of Map
.
In addition the Map
and HashMap
types now have three typed Equals
methods:
Equals(x, y)
- usesEqDefault<V>
to compare the valuesEquals<EqV>(x, y) where EqV : struct, Eq<V>
EqualsKeys(x, y) - which compares the keys only (equivalent to
Equals<EqTrue>(x, y)`
Map
has also had similar changes made to CompareTo
ordering:
CompareTo(x, y)
- usesOrdDefault<V>
to compare the valuesCompareTo<OrdV>(x, y) where OrdV : struct, Ord<V>
CompareKeysTo(x, y) - which compares the keys only (equivalent to
CompareTo<OrdTrue>(x, y)`
On top of this HashSet<A>
now has some performance improvements due to it using a new backing type of TrieSet<A>
rather than the TrieMap<A, Unit>
.
Finally, there's improvements to the Union
serialisation system for code-gen. Thanks @StefanBertels
Happy new year!
Paul
Support for C# pattern-matching
Language-ext was created before the C# pattern-matching feature existed. The default way to match within lang-ext is to use the Match(...)
methods provided for most types.
There have been requests for the struct
types to become reference-types so sub-types can represent the cases of types like Option<A>
, Either<L, R>
, etc. I don't think this is the best way forward for a number of reasons that I won't go in to here, but it would obviously be good to support the C# in-built pattern-matching.
So, now most types have a Case
property, or in the case of delegate
types like Try<A>
, or in-built BCL types like Task<T>
: a Case()
extension method.
For example, this is how to match on an Option<int>
:
var option = Some(123);
var result = option.Case switch
{
SomeCase<int>(var x) => x,
_ => 0 // None
};
Next we can try matching on an Either<string, int>
:
var either = Right<string, int>(123);
var result = either.Case switch
{
RightCase<string, int>(var r) => r,
LeftCase<string, int>(var _) => 0,
_ => 0 // Bottom
};
This is where some of the issues of C#'s pattern-matching show up, they can get quite verbose compared to calling the Match
method.
For async
types you simply have to await
the Case
:
var either = RightAsync<string, int>(123);
var result = await either.Case switch
{
RightCase<string, int>(var r) => r,
LeftCase<string, int>(var _) => 0,
_ => 0 // Bottom
};
The delegate types need to use Case()
rather than Case
:
var tryOption = TryOption<int>(123);
var result = tryOption.Case() switch
{
SuccCase<int>(var r) => r,
FailCase<int>(var _) => 0,
_ => 0 // None
};
All collection types support Case
also, they all work with the same matching system and so the cases are always the same for all collection types:
static int Sum(Seq<int> seq) =>
seq.Case switch
{
HeadCase<int>(var x) => x,
HeadTailCase<int>(var x, var xs) => x + Sum(xs),
_ => 0 // Empty
};
Records code-gen
This is the first-pass release of the LanguageExt.CodeGen
feature for generating record types. This means there's no need to derive from Record<TYPE>
any more, and also allows records to be structs, which is a real bonus.
To create a new record, simply attach the [Record]
attribute to a partial class
or a partial struct
:
[Record]
public partial struct Person
{
public readonly string Forename;
public readonly string Surname;
}
You may also use properties:
[Record]
public partial struct Person
{
public string Forename { get; }
public string Surname { get; }
}
As well as computed properties:
[Record]
public partial struct Person
{
public string Forename { get; }
public string Surname { get; }
public string FullName => $"{Forename} {Surname}";
}
The features of the generated record are:
- Auto constructor provision
- Auto deconstructor provision
- Structural equality (with equality operators also)
- Structural ordering (with ordering operators also)
GetHashCode
provision- Serialisation
- Sensible default
ToString
implementation With
method for immutable transformationLens
fields for composing nested immutable type transformation
Coming soon (for both records and unions) is the ability to provide class-instances to override the default behaviour of equality, ordering, hash-code generation, and constructor validation.
The generated code looks like this:
[System.Serializable]
public partial struct Person : System.IEquatable<Person>, System.IComparable<Person>, System.IComparable, System.Runtime.Serialization.ISerializable
{
public Person(string Forename, string Surname)
{
this.Forename = Forename;
this.Surname = Surname;
}
public void Deconstruct(out string Forename, out string Surname)
{
Forename = this.Forename;
Surname = this.Surname;
}
public Person(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
this.Forename = (string)info.GetValue("Forename", typeof(string));
this.Surname = (string)info.GetValue("Surname", typeof(string));
}
public void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
info.AddValue("Forename", this.Forename);
info.AddValue("Surname", this.Surname);
}
public static bool operator ==(Person x, Person y) => x.Equals(y);
public static bool operator !=(Person x, Person y) => !(x == y);
public static bool operator>(Person x, Person y) => x.CompareTo(y) > 0;
public static bool operator <(Person x, Person y) => x.CompareTo(y) < 0;
public static bool operator >=(Person x, Person y) => x.CompareTo(y) >= 0;
public static bool operator <=(Person x, Person y) => x.CompareTo(y) <= 0;
public bool Equals(Person other)
{
if (LanguageExt.Prelude.isnull(other))
return false;
if (!default(LanguageExt.ClassInstances.EqDefault<string>).Equals(this.Forename, other.Forename))
return false;
if (!default(LanguageExt.ClassInstances.EqDefault<string>).Equals(this.Surname, other.Surname))
return false;
return true;
}
public override bool Equals(object obj) => obj is Person tobj && Equals(tobj);
public int CompareTo(object obj) => obj is Person p ? CompareTo(p) : 1;
public int CompareTo(Person other)
{
if (LanguageExt.Prelude.isnull(other))
return 1;
int cmp = 0;
cmp = default(LanguageExt.ClassInstances.OrdDefault<string>).Compare(this.Forename, other.Forename);
if (cmp != 0)
return cmp;
cmp = default(LanguageExt.ClassInstances.OrdDefault<string>).Compare(this.Surname, other.Surname);
if (cmp != 0)
return cmp;
return 0;
}
public override int GetHashCode()
{
const int fnvOffsetBasis = -2128831035;
const int fnvPrime = 16777619;
int state = fnvOffsetBasis;
unchecked
{
state = (default(LanguageExt.ClassInstances.EqDefault<string>).GetHashCode(this.Forename) ^ state) * fnvPrime;
state = (default(LanguageExt.ClassInstances.EqDefault<string>).GetHashCode(this.Surname) ^ state) * fnvPrime;
}
return state;
}
public override string ToString()
{
var sb = new System.Text.StringBuilder();
sb.Append("Person(");
sb.Append(LanguageExt.Prelude.isnull(Forename) ? $"Forename: [null]" : $"Forename: {Forename}");
sb.Append($", ");
sb.Append(LanguageExt.Prelude.isnull(Surname) ? $"Surname: [null]" : $"Surname: {Surname}");
sb.Append(")");
return sb.ToString();
}
public Person With(string Forename = null, string Surname = null) => new Person(Forename ?? this.Forename, Surname ?? this.Surname);
public static readonly Lens<Person, string> forename = Lens<Person, string>.New(_x => _x.Forename, _x => _y => _y.With(Forename: _x));
public static readonly Lens<Person, string> surname = Lens<Person, string>.New(_x => _x.Surname, _x => _y => _y.With(Surname: _x));
}
Discriminated Union code-generation [even more improvements!]
Continuing from the two releases [1],[2] this weekend relating to the new discriminated-union feature of language-ext...
There is now support for creating unions from abstract classes. Although this is slightly less terse than using interfaces, there is a major benefit: classes can contain operators and so the equality and ordering operators can be automatically generated.
So, as well as being able to create unions from interfaces like so:
[Union]
public interface Shape
{
Shape Rectangle(float width, float length);
Shape Circle(float radius);
Shape Prism(float width, float height);
}
You can now additionally create them from an abstract partial class
like so:
[Union]
public abstract partial class Shape
{
public abstract Shape Rectangle(float width, float length);
public abstract Shape Circle(float radius);
public abstract Shape Prism(float width, float height);
}
Which allows for:
Shape shape1 = ShapeCon.Circle(100);
Shape shape2 = ShapeCon.Circle(100);
Shape shape3 = ShapeCon.Circle(50);
Assert.True(shape1 == shape2);
Assert.False(shape2 == shape3);
Assert.True(shape2 > shape3);
Case classes are now sealed
rather than partial
. partial
opens the door to addition of fields and properties which could compromise the case-type. And so extension methods are the best way of adding functionality to the case-types.
To make all of this work with abstract classes I needed to remove the inheritance of Record<CASE_TYPE>
from each union case, and so now the generated code does the work of the Record
type at compile-time rather than at run time. It's lead to a slight explosion in the amount of generated code, but I guess it shows how hard it is to do this manually!
[System.Serializable]
public sealed class Rectangle : _ShapeBase, System.IEquatable<Rectangle>, System.IComparable<Rectangle>, System.IComparable
{
public readonly float Width;
public readonly float Length;
public override int _Tag => 1;
public Rectangle(float width, float length)
{
this.Width = width;
this.Length = length;
}
public void Deconstruct(out float Width, out float Length)
{
Width = this.Width;
Length = this.Length;
}
public Rectangle(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
Width = (float)info.GetValue("Width", typeof(float));
Length = (float)info.GetValue("Length", typeof(float));
}
public void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
info.AddValue("Width", Width);
info.AddValue("Length", Length);
}
public static bool operator ==(Rectangle x, Rectangle y) => ReferenceEquals(x, y) || (x?.Equals(y) ?? false);
public static bool operator !=(Rectangle x, Rectangle y) => !(x == y);
public static bool operator>(Rectangle x, Rectangle y) => !ReferenceEquals(x, y) && !ReferenceEquals(x, null) && x.CompareTo(y) > 0;
public static bool operator <(Rectangle x, Rectangle y) => !ReferenceEquals(x, y) && (ReferenceEquals(x, null) && !ReferenceEquals(y, null) || x.CompareTo(y) < 0);
public static bool operator >=(Rectangle x, Rectangle y) => ReferenceEquals(x, y) || (!ReferenceEquals(x, null) && x.CompareTo(y) >= 0);
public static bool operator <=(Rectangle x, Rectangle y) => ReferenceEquals(x, y) || (ReferenceEquals(x, null) && !ReferenceEquals(y, null) || x.CompareTo(y) <= 0);
public bool Equals(Rectangle other)
{
if (LanguageExt.Prelude.isnull(other))
return false;
if (!default(EqDefault<float>).Equals(Width, other.Width))
return false;
if (!default(EqDefault<float>).Equals(Length, other.Length))
return false;
return true;
}
public override bool Equals(object obj) => obj is Rectangle tobj && Equals(tobj);
public override bool Equals(Shape obj) => obj is Rectangle tobj && Equals(tobj);
public override int CompareTo(object obj) => obj is Shape p ? CompareTo(p) : 1;
public override int CompareTo(Shape obj) => obj is Rectangle tobj ? CompareTo(tobj) : obj is null ? 1 : _Tag.CompareTo(obj._Tag);
public int CompareTo(Rectangle other)
{
if (LanguageExt.Prelude.isnull(other))
return 1;
int cmp = 0;
cmp = default(OrdDefault<float>).Compare(Width, other.Width);
if (cmp != 0)
return cmp;
cmp = default(OrdDefault<float>).Compare(Length, other.Length);
if (cmp != 0)
return cmp;
return 0;
}
public override int GetHashCode()
{
const int fnvOffsetBasis = -2128831035;
const int fnvPrime = 16777619;
int state = fnvOffsetBasis;
unchecked
{
state = (default(EqDefault<float>).GetHashCode(Width) ^ state) * fnvPrime;
state = (default(EqDefault<float>).GetHashCode(Length) ^ state) * fnvPrime;
}
return state;
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append("Rectangle(");
sb.Append(LanguageExt.Prelude.isnull(Width) ? $"Width: [null]" : $"Width: {Width}");
sb.Append($", ");
sb.Append(LanguageExt.Prelude.isnull(Length) ? $"Length: [null]" : $"Length: {Length}");
sb.Append(")");
return sb.ToString();
}
public Rectangle With(float? Width = null, float? Length = null) => new Rectangle(Width ?? this.Width, Length ?? this.Length);
public static readonly Lens<Rectangle, float> width = Lens<Rectangle, float>.New(_x => _x.Width, _x => _y => _y.With(Width: _x));
public static readonly Lens<Rectangle, float> length = Lens<Rectangle, float>.New(_x => _x.Length, _x => _y => _y.With(Length: _x));
}
[System.Serializable]
public sealed class Circle : _ShapeBase, System.IEquatable<Circle>, System.IComparable<Circle>, System.IComparable
{
public readonly float Radius;
public override int _Tag => 2;
public Circle(float radius)
{
this.Radius = radius;
}
public void Deconstruct(out float Radius)
{
Radius = this.Radius;
}
public Circle(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
Radius = (float)info.GetValue("Radius", typeof(float));
}
public void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
info.AddValue("Radius", Radius);
}
public static bool operator ==(Circle x, Circle y) => ReferenceEquals(x, y) || (x?.Equals(y) ?? false);
public static bool operator !=(Circle x, Circle y) => !(x == y);
public static bool operator>(Circle x, Circle y) => !ReferenceEquals(x, y) && !ReferenceEquals(x, null) && x.CompareTo(y) > 0;
public static bool operator <(Circle x, Circle y) => !ReferenceEquals(x, y) && (ReferenceEquals(x, null) && !ReferenceEquals(y, null) || x.CompareTo(y) < 0);
public static bool operator >=(Circle x, Circle y) => ReferenceEquals(x, y) || (!ReferenceEquals(x, null) && x.CompareTo(y) >= 0);
public static bool operator <=(Circle x, Circle y) => ReferenceEquals(x, y) || (ReferenceEquals(x, null) && !ReferenceEquals(y, null) || x.CompareTo(y) <= 0);
public bool Equals(Circle other)
{
if (LanguageExt.Prelude.isnull(other))
return false;
if (!default(EqDefault<float>).Equals(Radius, other.Radius))
return false;
return true;
}
public override bool Equals(object obj) => obj is Circle tobj && Equals(tobj);
public override bool Equals(Shape obj) => obj is Circle tobj && Equals(tobj);
public override int CompareTo(object obj) => obj is Shape p ? CompareTo(p) : 1;
public override int CompareTo(Shape obj) => obj is Circle tobj ? CompareTo(tobj) : obj is null ? 1 : _Tag.CompareTo(obj._Tag);
public int CompareTo(Circle other)
{
if (LanguageExt.Prelude.isnull(other))
return 1;
int cmp = 0;
cmp = default(OrdDefault<float>).Compare(Radius, other.Radius);
if (cmp != 0)
return cmp;
return 0;
}
public override int GetHashCode()
{
const int fnvOffsetBasis = -2128831035;
const int fnvPrime = 16777619;
int state = fnvOffsetBasis;
unchecked
{
state = (default(EqDefault<float>).GetHashCode(Radius) ^ state) * fnvPrime;
}
return state;
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append("Circle(");
sb.Append(LanguageExt.Prelude.isnull(Radius) ? $"Radius: [null]" : $"Radius: {Radius}");
sb.Append(")");
return sb.ToString();
}
public Circle With(float? Radius = n...
Discriminated Union code-generation [improvements]
Following on from last night's discriminated union feature release, I have added some additional features to the code-gen. The full-set of features available now are:
- Structural equality (via
lhs.Equals(rhs)
- due to the base-type being aninterface
) GetHashCode()
implementationToString()
implementation- Deconstructor method implemented for all union case types
With
implemented for all union case types- Lenses for all fields within a case type
- Improved error reporting for the code-gen system as a whole
And so, now this:
[Union]
public interface Shape
{
Shape Rectangle(float width, float length);
Shape Circle(float radius);
Shape Prism(float width, float height);
}
Will generate this:
public partial class Rectangle : LanguageExt.Record<Rectangle>, Shape
{
public readonly float Width;
public readonly float Length;
public Rectangle(float width, float length)
{
this.Width = width;
this.Length = length;
}
public void Deconstruct(out float Width, out float Length)
{
Width = this.Width;
Length = this.Length;
}
public Rectangle With(float? Width = null, float? Length = null) => new Rectangle(Width ?? this.Width, Length ?? this.Length);
public static readonly Lens<Rectangle, float> width = Lens<Rectangle, float>.New(_x => _x.Width, _x => _y => _y.With(Width: _x));
public static readonly Lens<Rectangle, float> length = Lens<Rectangle, float>.New(_x => _x.Length, _x => _y => _y.With(Length: _x));
Shape Shape.Rectangle(float width, float length) => throw new System.NotSupportedException();
Shape Shape.Circle(float radius) => throw new System.NotSupportedException();
Shape Shape.Prism(float width, float height) => throw new System.NotSupportedException();
}
public partial class Circle : LanguageExt.Record<Circle>, Shape
{
public readonly float Radius;
public Circle(float radius)
{
this.Radius = radius;
}
public void Deconstruct(out float Radius)
{
Radius = this.Radius;
}
public Circle With(float? Radius = null) => new Circle(Radius ?? this.Radius);
public static readonly Lens<Circle, float> radius = Lens<Circle, float>.New(_x => _x.Radius, _x => _y => _y.With(Radius: _x));
Shape Shape.Rectangle(float width, float length) => throw new System.NotSupportedException();
Shape Shape.Circle(float radius) => throw new System.NotSupportedException();
Shape Shape.Prism(float width, float height) => throw new System.NotSupportedException();
}
public partial class Prism : LanguageExt.Record<Prism>, Shape
{
public readonly float Width;
public readonly float Height;
public Prism(float width, float height)
{
this.Width = width;
this.Height = height;
}
public void Deconstruct(out float Width, out float Height)
{
Width = this.Width;
Height = this.Height;
}
public Prism With(float? Width = null, float? Height = null) => new Prism(Width ?? this.Width, Height ?? this.Height);
public static readonly Lens<Prism, float> width = Lens<Prism, float>.New(_x => _x.Width, _x => _y => _y.With(Width: _x));
public static readonly Lens<Prism, float> height = Lens<Prism, float>.New(_x => _x.Height, _x => _y => _y.With(Height: _x));
Shape Shape.Rectangle(float width, float length) => throw new System.NotSupportedException();
Shape Shape.Circle(float radius) => throw new System.NotSupportedException();
Shape Shape.Prism(float width, float height) => throw new System.NotSupportedException();
}
public static partial class ShapeCon
{
public static Shape Rectangle(float width, float length) => new Rectangle(width, length);
public static Shape Circle(float radius) => new Circle(radius);
public static Shape Prism(float width, float height) => new Prism(width, height);
}
Discriminated Union code-generation
In this release the code-generation story has been extended to support sum-types (also known as 'discriminated unions', 'union types', or 'case types').
Simply declare an interface
with the attribute [Union]
where all methods declared in the interface return the type of the interface, i.e.
[Union]
public interface Maybe<A>
{
Maybe<A> Just(A value);
Maybe<A> Nothing();
}
It has similar behaviour to this, in F#:
type Maybe<'a> =
| Just of 'a
| Nothing
In the above example, two case-types classes will be created Just<A>
and Nothing<A>
as well as static
constructor class called Maybe
:
var maybe = Maybe.Just(123);
var res = maybe switch
{
Just<int> just => just.Value,
Nothing<int> _ => 0
};
This is the generated code:
public partial class Just<A> : LanguageExt.Record<Just<A>>, Maybe<A>
{
public readonly A Value;
Maybe<A> Maybe<A>.Just(A value) => throw new System.NotSupportedException();
Maybe<A> Maybe<A>.Nothing() => throw new System.NotSupportedException();
public Just(A value)
{
Value = value;
}
}
public partial class Nothing<A> : LanguageExt.Record<Nothing<A>>, Maybe<A>
{
Maybe<A> Maybe<A>.Just(A value) => throw new System.NotSupportedException();
Maybe<A> Maybe<A>.Nothing() => throw new System.NotSupportedException();
public Nothing()
{
}
}
public static partial class Maybe
{
public static Maybe<A> Just<A>(A value) => new Just<A>(value);
public static Maybe<A> Nothing<A>() => new Nothing<A>();
}
The generated code is relatively basic at the moment. It will be extended to support abstract class
types and will auto-generate structural equality behaviour as well as other useful behaviours. But for now this is a super-quick way to generate the cases for a union-type and have a simple way of constructing them.
The generated types are all partial
and can therefore be extended trivially.
Here's another simple example:
[Union]
public interface Shape
{
Shape Rectangle(float width, float length);
Shape Circle(float radius);
Shape Prism(float width, float height);
}
And the generated code:
public partial class Rectangle : LanguageExt.Record<Rectangle>, Shape
{
public readonly float Width;
public readonly float Length;
Shape Shape.Rectangle(float width, float length) => throw new System.NotSupportedException();
Shape Shape.Circle(float radius) => throw new System.NotSupportedException();
Shape Shape.Prism(float width, float height) => throw new System.NotSupportedException();
public Rectangle(float width, float length)
{
Width = width;
Length = length;
}
}
public partial class Circle : LanguageExt.Record<Circle>, Shape
{
public readonly float Radius;
Shape Shape.Rectangle(float width, float length) => throw new System.NotSupportedException();
Shape Shape.Circle(float radius) => throw new System.NotSupportedException();
Shape Shape.Prism(float width, float height) => throw new System.NotSupportedException();
public Circle(float radius)
{
Radius = radius;
}
}
public partial class Prism : LanguageExt.Record<Prism>, Shape
{
public readonly float Width;
public readonly float Height;
Shape Shape.Rectangle(float width, float length) => throw new System.NotSupportedException();
Shape Shape.Circle(float radius) => throw new System.NotSupportedException();
Shape Shape.Prism(float width, float height) => throw new System.NotSupportedException();
public Prism(float width, float height)
{
Width = width;
Height = height;
}
}
public static partial class ShapeCon
{
public static Shape Rectangle(float width, float length) => new Rectangle(width, length);
public static Shape Circle(float radius) => new Circle(radius);
public static Shape Prism(float width, float height) => new Prism(width, height);
}
NOTE: The code-gen doesn't yet support .NET Core 3.0 - I'm still waiting for the Roslyn code-gen project to be updated. If it isn't forthcoming soon, I'll look for other options.