Bot releases are hidden (Show)
Published by louthy about 3 years ago
This release features improvements to the concurrency elements of language-ext:
Atom
- atomic referencesSTM
/ Ref
- software-transactional memory systemAtomHashMap
- new atomic data-structureAtom
and STM
The atomic references system (Atom
) that wraps a value and allows for atomic updates; and the STM
system that allows many Ref
values (that also wrap a value, but instead work within an atomic sync
transaction), have both been updated to never give up trying to resolve a conflict.
Previously they would fail after 500 attempts, but now any conflicts will cause the conflicting threads to back-off and eventually yield control so that the other thread(s) they were in conflict with eventually win and can update the atomic reference. This removes the potential time-bomb buried deep within the atomic references system, and creates a robust transactional system.
AtomHashMap<K, V>
One pattern I noted I was doing quite a lot was wrapping HashMap
in an atom, usually for shared cached values:
var atomData = Atom(HashMap<string, int>());
atomData.Swap(static hm => hm.AddOrUpdate("foo", 123));
It struck me that it would be very useful to have atomic versions of all of the collection data-structures of language-ext. The first one is AtomHashMap
. And so, instead of the code above, we can now write:
var atomData = AtomHashMap<string, int>();
atomData.AddOrUpdate("foo", 123);
All operations on AtomHashMap
are atomic and lock-free. The underling data structure is still an immutable HashMap
. It's simply the reference to the HashMap
that gets protected by AtomHashMap
, preventing two threads updating the data structure with stale data.
The main thing to understand with AtomHashMap
is that if a conflict occurs on update, then any transformational operation is re-run with the new state of the data-structure. Obviously conflicts are rare on high-performance CPUs, and so we save processing time from not taking locks on every operation, at the expense of occasional re-running of operations when conflicts arise.
Swap
AtomHashMap
also supports Swap
, which allows for more complex atomic operations on the underlying HashMap
. For example, if your update operation relies on data within the AtomHashMap
, then you might want to consider wrapping everything within a Swap
call to allow for fully idempotent transformations:
atomData.Swap(data => data.Find("foo").Case switch
{
int x => data.SetItem("foo", x + 1),
_ => data.Add("foo", 1)
});
NOTE: The longer you spend inside a
Swap
function, the higher the risk of conflicts, and so try to make sure you do the bare minimum within swap that will facilitate your idempotent operation.
Most operations on AtomHashMap
are in-place, i.e. they update the underlying HashMap
atomically. However, some functions like Map
, Filter
, Select
, Where
are expected to process the data-structure into a new data-structure. This is usually wanted, but we also want in-place filtering and mapping:
// Only keeps items with a value > 10 in the AtomHashMap
atomData.FilterInPlace(x => x > 10);
// Maps all items in the AtomHashMap
atomData.MapInPlace(x => x + 10);
The standard Map
, Filter
, etc. all still exist and work in the 'classic' way of generating a new data structure.
ToHashMap
At any point if you need to take a snapshot of what's in the AtomHashMap
you can all:
HashMap<string, int> snapshot = atomData.ToHashMap();
This is a zero allocation, zero time (well in the order of nanoseconds), operation. And so we can easily take snapshots and work on those whilst the atomic data structure can carry on being mutated without consequence.
As well as AtomHashMap<K, V>
there's also AtomHashMap<EqK, K, V>
which maps to HashMap<EqK, K, V>
.
This is just the beginning of the new Atom based data-structures. So watch this space!
Published by louthy over 3 years ago
This release features updates to the [Union]
feature of language-ext (discriminated union generator) as well as support for iOS Xamarin runtime code-gen
Match
function for each unionMap
function for each union.ToString
and GetHashCode
- allowing for your own bespoke implementationsRecord<A>
, Range
, and other types) now falls back to building implementations with Expression
.Match
Unions can already be pattern-matched using the C# switch
statement and switch
expression. Now you can use the generated Match
function like other built-in types (such as Option
, Either
, etc.). This match function enforces completeness checking, which the C# switch
can't do.
There are two strategies to generating the Match
function:
[Union]
type is an interface
, then the generated Match
function will be an extension-method. This requires your union-type to be a top-level declaration (i.e. not nested within another class). This may be a breaking change for you.It looks like this:
public static RETURN Match<A, RETURN>(this Maybe<A> ma, Func<Just<A>, RETURN> Just, Func<Nothing<A>, RETURN> Nothing) =>
ma switch
{
Just<A> value => Just(value),
Nothing<A> value => Nothing(value),
_ => throw new LanguageExt.ValueIsNullException()
};
[Union]
type is an abstract partial class
, then the generated Match
function will be an instance-method. This doesn't have the limitation of the interface
approach.Then the Match
instance methods look like this:
// For Just<A>
public override RETURN Match<RETURN>(Func<Just<A>, RETURN> Just, Func<Nothing<A>, RETURN> Nothing) =>
Just(this);
// For Nothing<A>
public override RETURN Match<RETURN>(Func<Just<A>, RETURN> Just, Func<Nothing<A>, RETURN> Nothing) =>
Nothing(this);
And so, if you still need unions nested within other classes, switch the interface to an abstract class:
This is an example of an interface based union:
[Union]
internal interface Maybe<A>
{
Maybe<A> Just(A value);
Maybe<A> Nothing();
}
This is the equivalent as an abstract class:
[Union]
public abstract partial class Maybe<A>
{
public abstract Maybe<A> Just(A value);
public abstract Maybe<A> Nothing();
}
Map
One thing that is true of all algebraic-data-types (of which category discriminated-unions fall into); is that there is exactly one way to generate a functor Map
function (Select
in LINQ) it's known as a theorem for free. And so the implementation for Map
and Select
are now provided by default by the code-gen.
So, for the example below:
[Union]
public abstract partial class Maybe<A>
{
public abstract Maybe<A> Just(A value);
public abstract Maybe<A> Nothing();
}
This code is auto-generated:
public static Maybe<B> Map<A, B>(this Maybe<A> ma, System.Func<A, B> f) =>
ma switch
{
Just<A> _v => new Just<B>(f(_v.Value)),
Nothing<B> _v => new Nothing<B>(),
_ => throw new System.NotSupportedException()
};
public static Maybe<B> Select<A, B>(this Maybe<A> ma, System.Func<A, B> f) =>
ma switch
{
Just<A> _v => new Just<B>(f(_v.Value)),
Nothing<B> _v => new Nothing<B>(),
_ => throw new System.NotSupportedException()
};
This will currently only support single generic argument unions, it will be expanded later to provide a Map
function for each generic argument. The other limitation is that if any of the cases have their own generic arguments, then the Map
function won't be generated. I expect this to cover a large number of use-cases though.
Any problems please report in the repo Issues.
Paul
Published by louthy over 3 years ago
I've done some refactoring of the Aff
and Eff
monads as I slowly progress toward a v4.0
release of language-ext:
RunIO
has been renamed to Run
HasCancel
has been rationalised to only use non-Eff and non-Aff properties
MapAsync
on the Eff
monads
Aff
and Eff
property in the Has*
traits, as an Eff
can be MapAsync
d into an Aff
efficientlyEff<RT, FileIO> FileEff
(obviously different types based on your trait)System
namespace have now been factored out into a new library LanguageExt.Sys
System
IO operationsFinally, I'm starting to document the Aff
and Eff
usage. This will be fleshed out more over the next few weeks.
All deployed to nu-get now.
Published by louthy almost 4 years ago
The Case
feature of the collection and union-types has changed, previously it would wrap up the state of the collection or union type into something that could be pattern-matched with C#'s new switch
expression. i.e.
var result = option.Case switch
{
SomeCase<A> (var x) => x,
NoneCase<A> _ => 0
}
The case wrappers have now gone, and the raw underlying value is returned:
var result = option.Case switch
{
int x => x,
_ => 0
};
The first form has an allocation overhead, because the case-types, like SomeCase
needed allocating each time. The new version has an allocation overhead only for value-types, as they are boxed. The classic way of matching, with Match(Some: x => ..., None: () => ...)
also has to allocate the lambdas, so there's a potential saving here by using this form of matching.
This also plays nice with the is
expression:
var result = option.Case is string name
? $"Hello, {name}"
: "Hello stranger";
There are a couple of downsides, but but I think they're worth it:
object
is the top-type for all types in C#, so you won't get compiler errors if you match with something completely incompatible with the bound valueEither
you lose the discriminator of Left
and Right
, and so if both cases are the same type, it's impossible to discriminate. If you need this, then the classic Match
method should be used.Collection types all have 3 case states:
null
Count == 1
will return A
Count > 1
will return (A Head, Seq<A> Tail)
For example:
static int Sum(Seq<int> values) =>
values.Case switch
{
null => 0,
int x => x,
(int x, Seq<int> xs) => x + Sum(xs),
};
NOTE: The tail of all collection types becomes
Seq<A>
, this is becauseSeq
is much more efficient at walking collections, and so all collection types are wrapped in a lazy-Seq. Without this, the tail would be rebuilt (reallocated) on every match; for recursive functions like the one above, that would be very expensive.
Published by louthy over 4 years ago
An ongoing thorn in my side has been the behaviour of Traverse
and Sequence
for certain pairs of monadic types (when nested). These issues document some of the problems:
https://github.com/louthy/language-ext/issues/552
https://github.com/louthy/language-ext/issues/651
The Traverse
and Sequence
functions were previously auto-generated by a T4 template, because for 25 monads that's 25 * 25 * 2 = 1250
functions to write. In practice it's a bit less than that, because not all nested monads should have a Traverse
and Sequence
function, but it is in the many hundreds of functions.
Because the same issue kept popping up I decided to bite the bullet and write them all by hand. This has a number of benefits:
Traverse
and Sequence
working most of the time, but not in all cases.Traverse
and Sequence
on list/sequence types. The generic T4 code-gen had to create singleton sequences and the concat them, which was super inefficient and could cause stack overflows. Often now I can pre-allocate an array and use a much faster imperative implementation with sequential memory access. Where possible I've tried to avoid nesting lambdas, again in the quest for performance but also to reduce the amount of GC objects created. I expect a major performance boost from these changes.Seq
and IEnumerable
when paired with async
types like Task
, OptionAsync
, etc. can now have bespoke behaviour to better handle the concurrency requirements (These types now have TraverseSerial
and SequenceSerial
which process tasks in a sequence one-at-a-time, and TraverseParallel
and SequenceParallel
which processes tasks in a sequence concurrently with a window of running tasks - that means it's possible to stop the Traverse
or Sequence
operation from thrashing the scheduler.Those are all lovely things, but the problem with writing several hundred functions manually is that there's gonna be bugs in there, especially as I've implemented them in the most imperative way I can to get the max performance out of them.
I have just spent the past three days writing these functions, and frankly, it was pretty soul destroying experience - the idea of writing several thousand unit tests fills me with dread; and so if any of you lovely people would like to jump in and help build some unit tests then I would be eternally grateful.
Sharing the load on this one would make sense. If you've never contributed to an open-source project before then this is a really good place to start!
I have...
3.4.14-beta
- so if you have unit tests that use Traverse
and Sequence
then any feedback on the stability of your tests would be really helpful.Things to know
Traverse
and Sequence
take a nested monadic type of the form MonadOuter<MonadInner<A>>
and flips it so the result is MonadInner<MonadOuter<A>>
Try<Option<A>>
would return Option<Try<A>>.None
if the outer Try
was in a Fail
state.Seq<Option<A>>
would return an Option<Seq<A>>.None
if any of the Options
in the Seq
were None
.Bottom
. For example: Either<Error, Try<A>>
. The new system now knows that the language-ext Error
type contains an Exception
and can therefore be used when constructing Try<Either<Error, A>>
Seq
or IEnumerable
. Seq
and IEnumerable
do have windows for throttling the consumption though.Option
combined with other types that have an error value (like Option<Try<A>>
, Option<Either<L, R>>
, etc.) will put None
into the resulting type (Try<Option<A>>(None)
, Either<L, Option<A>>(None)
if the outer type is None
- this is because there is no error value to construct an Exception
or L
value - and so the only option is to either return Bottom
or a success value with None
in it, which I think is slightly more useful. This behaviour is different from the old system. This decision is up for debate, and I'm happy to have it - the choices are: remove the pairing altogether (so there is no Traverse
or Sequence
for those types) or return None
as described above
Obviously, it helps if you understand this code, what it does and how it should work. I'll make some initial tests over the next few days as guidance.
Published by louthy over 4 years ago
Free monads allow the programmer to take a functor and turn it into a monad for free.
The [Free]
code-gen attribute provides this functionality in C#.
Below, is a the classic example of a Maybe
type (also known as Option
, here we're using the Haskell naming parlance to avoid confusion with the language-ext type).
[Free]
public interface Maybe<A>
{
[Pure] A Just(A value);
[Pure] A Nothing();
public static Maybe<B> Map<B>(Maybe<A> ma, Func<A, B> f) => ma switch
{
Just<A>(var x) => Maybe.Just(f(x)),
_ => Maybe.Nothing<B>()
};
}
Click here to see the generated code
The Maybe<A>
type can then be used as a monad:
var ma = Maybe.Just(10);
var mb = Maybe.Just(20);
var mn = Maybe.Nothing<int>();
var r1 = from a in ma
from b in mb
select a + b; // Just(30)
var r2 = from a in ma
from b in mb
from _ in mn
select a + b; // Nothing
And so, in 11 lines of code, we have created a Maybe
monad that captures the short-cutting behaviour of Nothing
.
But, actually, it's possible to do this in fewer lines of code:
[Free]
public interface Maybe<A>
{
[Pure] A Just(A value);
[Pure] A Nothing();
}
If you don't need to capture bespoke rules in the Map
function, the code-gen will build it for you.
A monad, a functor, and a discriminated union in 6 lines of code. Nice.
As with the discriminated-unions, [Free]
types allow for deconstructing the values when pattern-maching:
var txt = ma switch
{
Just<int> (var x) => $"Value is {x}",
_ => "No value"
};
The type 'behind' a free monad (in Haskell or Scala for example) usually has one of two cases:
Pure
Free
Pure
is what we've used so far, and that's why Just
and Nothing
had the Pure
attribute before them:
[Pure] A Just(A value);
[Pure] A Nothing();
They can be considered terminal values. i.e. just raw data, nothing else. The code generated works in exactly the same way as the common types in language-ext, like Option
, Either
, etc. However, if the [Pure]
attribute is left off the method-declaration then we gain an extra field in the generated case type: Next
.
Next
is a Func<*, M<A>>
- the *
will be the return type of the method-declaration.
For example:
[Free]
public interface FreeIO<T>
{
[Pure] T Pure(T value);
[Pure] T Fail(Error error);
string ReadAllText(string path);
Unit WriteAllText(string path, string text);
}
Click here to see the generated code
If we look at the generated code for the ReadAllText
case (which doesn't have a [Pure]
attribute), then we see that the return type of string
has now been injected into this additional Next
function which is provided as the last argument.
public sealed class ReadAllText<T> : FreeIO<T>, System.IEquata...
{
public readonly string Path;
public readonly System.Func<string, FreeIO<T>> Next;
public ReadAllText(string Path, System.Func<string, FreeIO<T>> Next)
{
this.Path = Path;
this.Next = Next;
}
Why is all this important? Well, it allows for actions to be chained together into a continuations style structure. This is useful for building a sequence of actions, very handy for building DSLs.
var dsl = new ReadAllText<Unit>("I:\\temp\\test.txt",
txt => new WriteAllText<Unit>("I:\\temp\\test2.txt", txt,
_ => new Pure<Unit>(unit)));
You should be able to see now why the [Pure]
types are terminal values. They are used at the end of the chain of continuations to signify a result.
But that's all quite ugly, so we can leverage the monadic aspect of the type:
var dsl = from t in FreeIO.ReadAllText("I:\\temp\\test.txt")
from _ in FreeIO.WriteAllText("I:\\temp\\test2.txt", t)
select unit;
The continuation itself doesn't do anything, it's just a pure data-structure representing the actions of the DSL. And so, we need an interpreter to run it (which you write). This is a simple example:
public static Either<Error, A> Interpret<A>(FreeIO<A> ma) => ma switch
{
Pure<A> (var value) => value,
Fail<A> (var error) => error,
ReadAllText<A> (var path, var next) => Interpret(next(Read(path))),
WriteAllText<A> (var path, var text, var next) => Interpret(next(Write(path, text))),
};
static string Read(string path) =>
File.ReadAllText(path);
static Unit Write(string path, string text)
{
File.WriteAllText(path, text);
return unit;
}
We can then run it by passing it the FreeIO<A>
value:
var result = Interpret(dsl);
Notice how the result type of the interpreter is Either
. We can use any result type we like, for example we could make the interpreter asynchronous:
public static async Task<A> InterpretAsync<A>(FreeIO<A> ma) => ma switch
{
Pure<A> (var value) => value,
Fail<A> (var error) => await Task.FromException<A>(error),
ReadAllText<A> (var path, var next) => await InterpretAsync(next(await File.ReadAllTextAsync(path))),
WriteAllText<A> (var path, var text, var next) => await InterpretAsync(next(await File.WriteAllTextAsync(path, text).ToUnit())),
};
Which can be run in a similar way, but asynchronously:
var res = await InterpretAsync(dsl);
And so, the implementation of the interpreter is up to you. It can also take extra arguments so that state can be carried through the operations. In fact it's very easy to use the interpreter to bury all the messy stuff of your application (the IO, maybe some ugly state management, etc.) in one place. This then allows the code itself (that works with the free-monad) to be referentialy transparent.
Another trick is to create a mock interpreter for unit-testing code that uses IO without having to ever do real IO. The logic gets tested, which is what is often the most important aspect of unit testing, but not real IO occurs. The arguments to the interpreter can be the mocked state.
Some caveats though:
Caveats aside, the free-monad allows for complete abstraction from side-effects, and makes all operations pure. This is incredibly powerful.
Published by louthy over 4 years ago
Unfortunately, the previous release with the latest CodeGeneration.Roslyn
build caused problems due to possible bugs in the CodeGeneration.Roslyn
plugin system. These issues only manifested in the nuget package version of the LanguageExt.CodeGen
and not in my project-to-project tests, giving a false sense of security.
After a lot of head-scratching, and attempts at making it work, it seems right to roll it back.
This also means rolling back to netstandard2.0
so that the old code-gen can work. And so, I have had to also remove the support for IAsyncEnumerable
with OptionAsync
and EitherAsync
until this is resolved.
Apologies for anyone who wasted time on the last release and who might be inconvenienced by the removal of IAsyncEnumerable
support. I tried so many different approaches and none seemed to be working.
Issues resolved:
Improvements:
Map
and Lst
Any further issues, please feel free to shout on the issues page or gitter.
Published by louthy over 4 years ago
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:
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.
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 functionAllow 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"
Hashable<A>
and HashableAsync<A>
type-classesHashable<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.
[Record]
and [Union]
code-genThe 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.
Published by louthy over 4 years ago
The attributes:
NonEq
- to opt out of equalityNonOrd
- to opt out of orderingNonShow
- to opt out of ToString
NonHash
- to opt out of GetHashCode
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
Published by louthy over 4 years ago
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.
Published by louthy almost 5 years ago
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 [ ]
.
Published by louthy almost 5 years ago
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)
- uses EqDefault<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)
- uses OrdDefault<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
Published by louthy almost 5 years ago
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
};
Published by louthy almost 5 years ago
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:
GetHashCode
provisionToString
implementationWith
method for immutable transformationLens
fields for composing nested immutable type transformationComing 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));
}
Published by louthy almost 5 years ago
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 = 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));
}
[System.Serializable]
public sealed class Prism : _ShapeBase, System.IEquatable<Prism>, System.IComparable<Prism>, System.IComparable
{
public readonly float Width;
public readonly float Height;
public override int _Tag => 3;
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(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
Width = (float)info.GetValue("Width", typeof(float));
Height = (float)info.GetValue("Height", typeof(float));
}
public void GetObjectData(System.Runtime.Serialization.SerializationInfo info, System.Runtime.Serialization.StreamingContext context)
{
info.AddValue("Width", Width);
info.AddValue("Height", Height);
}
public static bool operator ==(Prism x, Prism y) => ReferenceEquals(x, y) || (x?.Equals(y) ?? false);
public static bool operator !=(Prism x, Prism y) => !(x == y);
public static bool operator>(Prism x, Prism y) => !ReferenceEquals(x, y) && !ReferenceEquals(x, null) && x.CompareTo(y) > 0;
public static bool operator <(Prism x, Prism y) => !ReferenceEquals(x, y) && (ReferenceEquals(x, null) && !ReferenceEquals(y, null) || x.CompareTo(y) < 0);
public static bool operator >=(Prism x, Prism y) => ReferenceEquals(x, y) || (!ReferenceEquals(x, null) && x.CompareTo(y) >= 0);
public static bool operator <=(Prism x, Prism y) => ReferenceEquals(x, y) || (ReferenceEquals(x, null) && !ReferenceEquals(y, null) || x.CompareTo(y) <= 0);
public bool Equals(Prism other)
{
if (LanguageExt.Prelude.isnull(other))
return false;
if (!default(EqDefault<float>).Equals(Width, other.Width))
return false;
if (!default(EqDefault<float>).Equals(Height, other.Height))
return false;
return true;
}
public override bool Equals(object obj) => obj is Prism tobj && Equals(tobj);
public override bool Equals(Shape obj) => obj is Prism tobj && Equals(tobj);
public override int CompareTo(object obj) => obj is Shape p ? CompareTo(p) : 1;
public override int CompareTo(Shape obj) => obj is Prism tobj ? CompareTo(tobj) : obj is null ? 1 : _Tag.CompareTo(obj._Tag);
public int CompareTo(Prism 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(Height, other.Height);
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(Height) ^ state) * fnvPrime;
}
return state;
}
public override string ToString()
{
var sb = new StringBuilder();
sb.Append("Prism(");
sb.Append(LanguageExt.Prelude.isnull(Width) ? $"Width: [null]" : $"Width: {Width}");
sb.Append($", ");
sb.Append(LanguageExt.Prelude.isnull(Height) ? $"Height: [null]" : $"Height: {Height}");
sb.Append(")");
return sb.ToString();
}
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));
}
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);
}
[System.Serializable]
public abstract partial class Shape : IEquatable<Shape>, IComparable<Shape>, IComparable
{
public abstract int _Tag
{
get;
}
public abstract int CompareTo(object obj);
public abstract int CompareTo(Shape other);
public abstract bool Equals(Shape other);
public override bool Equals(object obj) => obj is Shape tobj && Equals(tobj);
public override int GetHashCode() => throw new System.NotSupportedException();
public static bool operator ==(Shape x, Shape y) => ReferenceEquals(x, y) || (x?.Equals(y) ?? false);
public static bool operator !=(Shape x, Shape y) => !(x == y);
public static bool operator>(Shape x, Shape y) => !ReferenceEquals(x, y) && !ReferenceEquals(x, null) && x.CompareTo(y) > 0;
public static bool operator <(Shape x, Shape y) => !ReferenceEquals(x, y) && (ReferenceEquals(x, null) && !ReferenceEquals(y, null) || x.CompareTo(y) < 0);
public static bool operator >=(Shape x, Shape y) => ReferenceEquals(x, y) || (!ReferenceEquals(x, null) && x.CompareTo(y) >= 0);
public static bool operator <=(Shape x, Shape y) => ReferenceEquals(x, y) || (ReferenceEquals(x, null) && !ReferenceEquals(y, null) || x.CompareTo(y) <= 0);
}
public abstract partial class _ShapeBase : Shape
{
public override Shape Rectangle(float width, float length) => throw new NotSupportedException();
public override Shape Circle(float radius) => throw new NotSupportedException();
public override Shape Prism(float width, float height) => throw new NotSupportedException();
}
This will soon be adapted to support a
[Record]
attribute for generating records at compile-time and remove the need to derive fromRecord<TYPE>
. That will meanstruct
records will be easy to create.
Published by louthy almost 5 years ago
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:
lhs.Equals(rhs)
- due to the base-type being an interface
)GetHashCode()
implementationToString()
implementationWith
implemented for all union case typesAnd 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);
}
Published by louthy almost 5 years ago
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.
Published by louthy about 5 years ago
Record
types now have an improved hash-code algorithm, based on the FNV 1a hashing algorithm
Reader
and RWS
code-generators will now look for existing methods with the same name as the one they're going to generate. If existing methods exist then the methods won't be generated.
Bind
implementations without having to build everything by hand.Map
, Select
, and SelectMany
are now implemented with Bind
. So they will also leverage any bespoke Bind
methods.
Where
is implemented with Filter
, which means providing a bespoke Filter
method will also update the Where
Added Match(Action<A> Succ, Action<Error> Fail)
for side-effecting matching to RWSResult
and ReaderResult
Added IfFailThrow()
to RWSResult
and ReaderResult
Bug fix: for RWS.Run
which was still returning a tuple after the refactor for better error handling. It now returns RWSResult
.
Bug fix: Where
implementation typo for Arr<A>
Thanks to: @alaendle and @EdruptCo for the fixes.
There is a new sample based on the famous Contoso app. @blakeSaucier has kindly converted it over to be more functional, using many of the features of language-ext. Going forward I will try and de-interface it and make it more pure and monadic, but this is a fantastic starting point to give developers guidance on how they can be more functional with their C# code.
Published by louthy about 5 years ago
This is a small update to the LanguageExt.CodeGen
package to improve the namespacing of types and methods in the generated RWS
and Reader
monad code (so you don't have to provide the namespaces manually).
Published by louthy about 5 years ago
As with the previous release that refactored the Reader
monad to have better error handling. I have now done the same for the RWS
monad.
RWS
doesn't now return a tuple and instead returns RWSResult<MonoidW, R, W, S, A>
which has a lot of the same functionality as ReaderResult<A>
but with additional functionality bespoke to the RWS
monad (ToReader()
, ToWriter()
, ToState()
).RWSResult
static class has been replaced and you should now use the RWS
and RWSFail
constructors in the Prelude
to construct the pure and failure monads.The LanguageExt.CodeGen
library has been updated to work with the new RWS
monad and is the easiest way to work with Reader
and RWS
monads (Writer
and State
will be added soon).
For those that have missed it, this:
namespace TestBed
{
[RWS(WriterMonoid: typeof(MSeq<string>),
Env: typeof(IO),
State: typeof(Person),
Constructor: "Pure",
Fail: "Error" )]
public partial struct Subsys<T>
{
}
}
Will generate:
namespace TestBed
{
public partial struct Subsys<T>
{
readonly internal LanguageExt.RWS<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T> __comp;
internal Subsys(LanguageExt.RWS<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T> comp) => __comp = comp;
public static Subsys<T> Pure(T value) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, value));
public static Subsys<T> Error() => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, LanguageExt.Common.Error.Bottom));
public static Subsys<T> Error(string message) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, LanguageExt.Common.Error.New(message)));
public static Subsys<T> Error(Exception exception) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, LanguageExt.Common.Error.New(exception)));
public static Subsys<T> Error(LanguageExt.Common.Error error) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, error));
public static Subsys<T> Error(string message, Exception exception) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, LanguageExt.Common.Error.New(message, exception)));
public Subsys<U> Map<U>(Func<T, U> f) => new Subsys<U>(__comp.Map(f));
public Subsys<U> Select<U>(Func<T, U> f) => new Subsys<U>(__comp.Map(f));
public Subsys<U> Bind<U>(Func<T, Subsys<U>> f) => new Subsys<U>(__comp.Bind(a => f(a).__comp));
public Subsys<U> SelectMany<U>(Func<T, Subsys<U>> f) => new Subsys<U>(__comp.Bind(a => f(a).__comp));
public Subsys<V> SelectMany<U, V>(Func<T, Subsys<U>> bind, Func<T, U, V> project) => new Subsys<V>(__comp.Bind(a => bind(a).__comp.Map(b => project(a, b))));
public (TryOption<T> Value, LanguageExt.Seq<string> Output, TestBed.Person State) Run(TestBed.IO env, TestBed.Person state) => __comp.Run(env, state);
public Subsys<T> Filter(Func<T, bool> f) => new Subsys<T>(__comp.Where(f));
public Subsys<T> Where(Func<T, bool> f) => new Subsys<T>(__comp.Where(f));
public Subsys<T> Do(Action<T> f) => new Subsys<T>(__comp.Do(f));
public Subsys<T> Strict() => new Subsys<T>(__comp.Strict());
public Seq<T> ToSeq(TestBed.IO env, TestBed.Person state) => __comp.ToSeq(env, state);
public Subsys<LanguageExt.Unit> Iter(Action<T> f) => new Subsys<LanguageExt.Unit>(__comp.Iter(f));
public Func<TestBed.IO, TestBed.Person, State> Fold<State>(State state, Func<State, T, State> f)
{
var self = this;
return (env, s) => self.__comp.Fold(state, f).Run(env, s).Value.IfNoneOrFail(state);
}
public Func<TestBed.IO, TestBed.Person, bool> ForAll(Func<T, bool> f)
{
var self = this;
return (env, s) => self.__comp.ForAll(f).Run(env, s).Value.IfNoneOrFail(false);
}
public Func<TestBed.IO, TestBed.Person, bool> Exists(Func<T, bool> f)
{
var self = this;
return (env, s) => self.__comp.Exists(f).Run(env, s).Value.IfNoneOrFail(false);
}
public Subsys<T> Local(Func<TestBed.IO, TestBed.IO> f) => new Subsys<T>(LanguageExt.Prelude.local<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>(__comp, f));
public Subsys<(T, U)> Listen<U>(Func<LanguageExt.Seq<string>, U> f) => new Subsys<(T, U)>(__comp.Listen(f));
public Subsys<T> Censor(Func<LanguageExt.Seq<string>, LanguageExt.Seq<string>> f) => new Subsys<T>(__comp.Censor(f));
}
public static partial class Subsys
{
public static Subsys<T> Pure<T>(T value) => Subsys<T>.Pure(value);
public static Subsys<T> Error<T>() => Subsys<T>.Error();
public static Subsys<T> Error<T>(string message) => Subsys<T>.Error(message);
public static Subsys<T> Error<T>(string message, Exception exception) => Subsys<T>.Error(message, exception);
public static Subsys<T> Error<T>(Exception exception) => Subsys<T>.Error(exception);
public static Subsys<T> Error<T>(LanguageExt.Common.Error error) => Subsys<T>.Error(error);
public static Subsys<T> asks<T>(Func<TestBed.IO, T> f) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, f(env)));
public static Subsys<TestBed.IO> ask => new Subsys<TestBed.IO>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, TestBed.IO>.New(state, env));
public static Subsys<TestBed.Person> get => new Subsys<TestBed.Person>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, TestBed.Person>.New(state, state));
public static Subsys<T> gets<T>(Func<TestBed.Person, T> f) => new Subsys<T>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, T>.New(state, f(state)));
public static Subsys<LanguageExt.Unit> put(TestBed.Person value) => new Subsys<LanguageExt.Unit>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, LanguageExt.Unit>.New(value, default(LanguageExt.Unit)));
public static Subsys<LanguageExt.Unit> modify(Func<TestBed.Person, TestBed.Person> f) => new Subsys<LanguageExt.Unit>((env, state) => LanguageExt.RWSResult<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, LanguageExt.Unit>.New(f(state), default(LanguageExt.Unit)));
public static Subsys<T> local<T>(Subsys<T> ma, Func<TestBed.IO, TestBed.IO> f) => ma.Local(f);
public static Subsys<T> Pass<T>(this Subsys<(T, Func<LanguageExt.Seq<string>, LanguageExt.Seq<string>>)> ma) => new Subsys<T>(ma.__comp.Pass());
public static Subsys<T> pass<T>(Subsys<(T, Func<LanguageExt.Seq<string>, LanguageExt.Seq<string>>)> ma) => new Subsys<T>(ma.__comp.Pass());
public static Subsys<(T, U)> listen<T, U>(Subsys<T> ma, Func<LanguageExt.Seq<string>, U> f) => ma.Listen(f);
public static Subsys<T> censor<T>(Subsys<T> ma, Func<LanguageExt.Seq<string>, LanguageExt.Seq<string>> f) => ma.Censor(f);
public static Subsys<LanguageExt.Unit> tell(LanguageExt.Seq<string> what) => new Subsys<LanguageExt.Unit>(tell<LanguageExt.ClassInstances.MSeq<string>, TestBed.IO, LanguageExt.Seq<string>, TestBed.Person, LanguageExt.Unit>(what));
public static Subsys<LanguageExt.Seq<string>> ReadAllLines(string fileName) => ask.Map(__env => __env.ReadAllLines(fileName));
public static Subsys<LanguageExt.Unit> WriteAllLines(string fileName, LanguageExt.Seq<string> lines) => ask.Map(__env => __env.WriteAllLines(fileName, lines));
public static Subsys<int> Zero => ask.Map(__env => __env.Zero);
public static Subsys<string> Name => get.Map(__env => __env.Name);
public static Subsys<string> Surname => get.Map(__env => __env.Surname);
}
}
Thereby making it much easier to work with the RWS
monad.