Releases: louthy/language-ext
Record types in C# !
The latest release of language-ext offers support for building 'record types' in C#.
It's no secret that implementing immutable record types, with structural equality, structural ordering, and efficient hashing solutions is a real manual head-ache of implementing Equals
, GetHashCode
, deriving from IEquatable<A>
, IComparable<A>
, and implementing the operators: ==
, !=
, <
, <=
, >
, >=
. It is a constant maintenance headache of making sure they're kept up to date when new fields are added to the type - with no compilation errors if you forget to do it.
Record<A>
This can now be achieved simply by deriving your type from Record<A>
where A
is the type you want to have structural equality and ordering. i.e.
public sealed class TestClass : Record<TestClass>
{
public readonly int W;
public readonly string X;
public readonly Guid Y;
public AnotherType Z { get; set; }
public TestClass(int w, string x, Guid y, AnotherType z)
{
W = w;
X = x;
Y = y;
Z = z;
}
}
This gives you Equals
, IEquatable.Equals
, IComparable.CompareTo
, GetHashCode
, operator==
, operator!=
, operator >
, operator >=
, operator <
, and operator <=
implemented by default. As well as a default ToString
implementation and an ISerializable
implementation.
Note that only fields or field backed properties are used in the structural comparisons and hash-code building. So if you want to use properties then they must not have any code in their getters or setters.
No reflection is used to achieve this result, the
Record
type builds the IL directly, and so it's as efficient as writing the code by hand.
There are some unit tests to see this in action.
Opting out
It's possible to opt various fields and properties out of the default behaviours using the following attributes:
Equals()
-OptOutOfEq
CompareTo()
-OptOutOfOrd
GetHashCode()
-OptOutOfHashCode
ToString()
-OptOutOfToString
- Serialization -
OptOutOfSerialization
(can also useNonSerializable
)
For example, here's a record type that opts out of various default behaviours:
public sealed class TestClass2 : Record<TestClass2>
{
[OptOutOfEq]
public readonly int X;
[OptOutOfHashCode]
public readonly string Y;
[OptOutOfToString]
public readonly Guid Z;
public TestClass2(int x, string y, Guid z)
{
X = x;
Y = y;
Z = z;
}
}
And some unit tests showing the result:
public void OptOutOfEqTest()
{
var x = new TestClass2(1, "Hello", Guid.Empty);
var y = new TestClass2(1, "Hello", Guid.Empty);
var z = new TestClass2(2, "Hello", Guid.Empty);
Assert.True(x == y);
Assert.True(x == z);
}
RecordType<A>
You can also use the 'toolkit' that Record<A>
uses to build this functionality in your own bespoke types (perhaps if you want to use this for struct
comparisons or if you can't derive directly from Record<A>
, or maybe you just want some of the functionality for ad-hoc behaviour):
The toolkit is composed of seven functions:
RecordType<A>.Hash(record);
This will provide the hash-code for the record of type A
provided. It can be used for your default GetHashCode()
implementation.
RecordType<A>.Equality(record, obj);
This provides structural equality with the record of type A
and the record of type object
. The types must match for the equality to pass. It can be used for your default Equals(object)
implementation.
RecordType<A>.EqualityTyped(record1, record2);
This provides structural equality with the record of type A
and another record of type A
. It can be used for your default Equals(a, b)
method for the IEquatable<A>
implementation.
RecordType<A>.Compare(record1, record2);
This provides a structural ordering comparison with the record of type A
and another record the record of type A
. It can be used for your default CompareTo(a, b)
method for the IComparable<A>
implementation.
RecordType<A>.ToString(record);
A default ToString
provider.
RecordType<A>.SetObjectData(record, serializationInfo);
Populates the fields of the record from the SerializationInfo
structure provided.
RecordType<A>.GetObjectData(record, serializationInfo);
Populates the SerializationInfo
structure from the fields of the record.
Below is the toolkit in use, it's used to build a struct
type that has structural equality, ordering, and hash-code implementation.
public class TestStruct : IEquatable<TestStruct>, IComparable<TestStruct>, ISerializable
{
public readonly int X;
public readonly string Y;
public readonly Guid Z;
public TestStruct(int x, string y, Guid z)
{
X = x;
Y = y;
Z = z;
}
TestStruct(SerializationInfo info, StreamingContext context) =>
RecordType<TestStruct>.SetObjectData(this, info);
public void GetObjectData(SerializationInfo info, StreamingContext context) =>
RecordType<TestStruct>.GetObjectData(this, info);
public override int GetHashCode() =>
RecordType<TestStruct>.Hash(this);
public override bool Equals(object obj) =>
RecordType<TestStruct>.Equality(this, obj);
public int CompareTo(TestStruct other) =>
RecordType<TestStruct>.Compare(this, other);
public bool Equals(TestStruct other) =>
RecordType<TestStruct>.EqualityTyped(this, other);
}
Type-classes
For anybody using the type-class and class-instance features of language-ext there are two new class instances to support record equality and ordering:
EqRecord<A>
OrdRecord<A>
So for example if you have two functions constrained to take Eq<A>
or Ord<A>
types:
public bool GenericEquals<EqA, A>(A x, A y) where EqA : struct, Eq<A> =>
default(EqA).Equals(x, y);
public int GenericCompare<OrdA, A>(A x, A y) where OrdA : struct, Ord<A> =>
default(OrdA).Compare(x, y);
Then you can call them with any record type:
var x = new TestClass(1, "Hello", Guid.Empty);
var y = new TestClass(1, "Hello", Guid.Empty);
var res1 = GenericEquals<EqRecord<TestClass>,TestClass>(x, y);
var res2 = GenericCompare<OrdRecord<TestClass>, TestClass>(x, y);
All on nu-get:
Minor release: Either updates
Either updates
Partition
to useValueTuple
Partition
/Lefts
/Rights
Seq
variants- Moved
Typeclass
attribute toLanguageExt.Attributes
All on nu-get:
Breaking change: Flipped Compose and BackCompose
The Func
extension methods Compose
and BackCompose
have been flipped. I felt they were the wrong way around, and that in this situation where the Prelude
function compose
works one way:
static Func<int, float> f = x => x * 3.0f;
static Func<float, bool> g = x => x > 4.0f;
var h = compose(f, g);
That the extension method should also go the same way:
var h = f.Compose(g);
It would also feel more natural when composing multiple functions:
var x = a.Compose(b).Compose(c).Compose(d);
So if you use the Compose
and BackCompose
extensions, then you need to flip them in your code.
Type-classes, newtype, and new collection-types
Version 2 Release Notes
Version 2.0 of Language-Ext is now in released, I was going to wait until I had time to rewrite the README docs, but I think that will take some time, and so I think it's best to get this live.
This is a major overhaul of every type in the system. I have also broken out the LanguageExt.Process
actor system into its own repo, it is now named Echo, so if you're using that you should head over to the repo and follow that. It's still in alpha - it's feature complete, it just needs more testing - so it's lagging behind at the moment. If you're using both lang-ext Core and the Echo Process system then wait until the Echo Process system is released before migrating to the new Core.
Version 2.0 of Language-Ext actually just started out as a branch where I was trying out a new technique for doing ad-hoc polymorphism in C# (think somewhere between Haskell typeclasses and Scala implicits).
I didn't expect it to lead to an entire re-write. So a word of warning, there are many areas that I know will be breaking changes, but some I don't. Breaking changes will 99% of the time be compile time errors (rather than changes in behaviour that silently affect your code). So although I don't expect any major issues, For any large projects I would put aside an afternoon to fix up any compilation breakages.
Often the breakages are for things like rectifying naming inconsistencies (for example some bi-map functions were named Map
, some named BiMap
, they're all now BiMap
), another example is that all collection types (Lst
, Map
, etc.) are now structs. So any code that does this will fail to compile:
Map<int, string> x = null;
The transformer extensions have been overhauled too (they provided overloads for nested monadic types, Option<Lst<A>>
for example). If you were cheating trying to get directly at values by calling Lift
or LiftUnsafe
, well, now you can't. It was a bad idea that was primarily to make the old transformer types work. So they're gone.
The overloads of Select
and SelectMany
are more restricted now, because combining different monadic types could lead to some type resolution issues with the compiler. You will now need to lift your types into the context of the LINQ expression (there are now lots of extensions to do that: ToOption
, ToTry
, etc.)
For the problems you will inevitablity have with upgrading to language-ext 2.0, you will also have an enormous amount of new benefits and possibilities.
My overriding goal with this library is to try and provide a safer environment in which to write C#. Version 1 was mostly trying to protect the programmer from null and mutable state. Version 2 is very much focussed on improving our lot when implementing abstract types.
Inheritance based polymorphism is pretty much accepted to be the worst performer in the polymorphic world. Our other option is parametric polymorphism (generics). With this release I have facilitated ad-hoc polymorphism with a little known technique in C#.
So for the first time it's possible to write numeric methods once for all numeric types or do structural equality testing that you can rely on.
Also there is support for the much more difficult higher-order polymorphic types like Monad<MA, A>
. LanguageExt 2.0 provides a fully type-safe and efficient approach to working with higher order types. So yes, you can now write functions that take monads, or functors, or applicatives, and return specialised values (rather than abstract or dynamic values). Instead of writing a function that takes an Option<A>
, you can write one that takes any monadic type, bind them, join them, map them, and return the concrete type that you pushed in.
Of course without compiler or runtime support for higher-order generics some hoops need to be jumped through (and I'm sure there will be some Haskell purist losing their shit over the approach). But at no point is the integrity of your types affected. Often the technique requires quite a large amount of generic argument typing, but if you want to write the super generic code, it's now possible. I don't know of any other library that provides this functionality.
This has allowed the transformer extensions to become more powerful too (because the code generator that emits them can now use the type-class/instance system). The Writer
monad can now work with any output type (as long as it has a Monoid
instance), so it's not limited to telling its output to an IEnumerable
, it can be a Lst
, a string
, an int
, or whatever Monoid
you specify.
Personally I find this very elegant and exciting. It has so much potential, but many will be put off by the amount of generic args typing they need to do. If anybody from the Rosyln team is reading this, please for the love of god help out with the issues around constraints and excessive specifying of generic arguments. The power is here, but needs more support.
Scroll down to the section on Ad-hoc polymorphism for more details.
Documentation
Full API documentation can be found here
Bug fixes
- Fix for
Lst.RemoveAt(index)
- certain tree arrangements caused this function to fail - Fix for
HSet
(nowHashSet
) constructor bug - constructing with an enumerable always failed Map
,Lst
,Set
,Option
,Either
, etc. All have serialisers that work with Json.NET (finally).
Examples: https://github.com/louthy/language-ext/blob/type-classes/LanguageExt.Tests/SerialisationTests.cs
New features - LanguageExt.Core
New collection types:
Type | Description |
---|---|
Seq<A> |
Cons-like, singly-linked list |
HashSet<A> |
Ordering is done by GetHashCode() . Existence testing is with EqualityComparer<A>.Default.Equals(a,b) |
HashMap<A, B> |
Ordering is done by GetHashCode() . Existence testing is with EqualityComparer<A>.Default.Equals(a,b) |
HashSet<EqA, A> where EqA : struct, Eq<A> |
Ordering is done by GetHashCode() . Existence testing is with default(EqA).Equals(a,b) |
HashMap<EqA, A, B> |
Ordering is done by GetHashCode() . Existence testing is with default(EqA).Equals(a,b) |
Set<OrdA, A> where OrdA : struct, Ord<A> |
Ordering is done by default(OrdA).Compare(a,b) . Existence testing is with default(OrdA).Equals(a,b) |
Map<EqA, A, B> |
Ordering is done by default(OrdA).Compare(a,b) . Existence testing is with default(OrdA).Equals(a,b) |
Arr<A> |
Immutable array. Has the same access speed as the built-in array type, but with immutable cells. Modification is expensive, due to the entire array being copied per operation (although for very small arrays this would be more efficient than Lst<T> or Set<T> ). |
Lst<PredList, A> where PredList : struct, Pred<ListInfo> |
This allows lists to run a predicate on the Count property of the list after construction. |
Lst<PredList, PredItem, A> where PredItem : struct, Pred<A> |
This allows lists to run a predicate on the Count property of the list after construction and on items as they're being added to the list. |
As you can see above there are new type-safe key versions of Set
, HashSet
, Map
, and HashMap
. Imagine you want to sort the value of a set of strings in a case-insensitive way (without losing information by calling value.ToLower()
).
var map = Set<TStringOrdinalIgnoreCase, string>(...)
The resulting type would be incompatible with:
Set<TString, string>, or Set<TStringOrdinal, string>
And is therefore more type-safe than just using Set. Examples
The two new predicate versions of Lst
allow for properties of the list to travel with the type. So for example this shows how you can enforce a list to be non-empty:
public int Product(Lst<NonEmpty, int> list) =>
list.Fold(1, (s, x) => s * x);
There are implicit conversion operators between Lst<A>
and Lst<PredList, A>
, and between Lst<A>
and Lst<PredList, PredItem, A>
. They don't need to reallocate the collection, but converting to a more constrained type will cause the validation to run. This is very light for constructing Lst<PredList, A>
, but will cause every item in the list to be validated for Lst<PredList, PredItem, A>
.
And so it's possible to do this:
Lst<int> list = List<int>();
var res = Product(list); // ArgumentOutOfRangeException
That will throw an ArgumentOutOfRangeException
because the list is empty. Whereas this is fine:
Lst<int> list = List<int>(1, 2, 3, 4, 5);
var res = Product(list); // 120
To construct the predicate list types directly, call:
Lst<NonEmpty, int> list = List<NonEmpty, int>(1, 2, 3, 4, 5);
The second type of predicate Lst
is Lst<PredList, PredItem, A>
. PredItem
is a predicate that's run against every item being added to the list. Once the item is in the list it won't be checked again (because it's an immutable list).
For example, this is a Lst
that can't be empty and won't accept null
items.
var x = List<NonEmpty, NonNullItems<string>, string>("1", "2", "3");
Obviously declaring types like this gets quite bulky quite quickly. So only using them for method arguments is definitely a good approach:
public string Divify(Lst<NonEmpty, NonNullItems<string>, string> items) =>
String.Join(items.Map(x => $"<div>{x}</div>"...
Seq - a better IEnumerable
A new feature in v2.0.45 beta
is the type Seq<A>
which derives from ISeq<A>
, which in turn is an IEnumerable<A>
.
It works very much like cons in functional languages (although not a real cons, as that's not a workable solution in C#). A Seq<A>
has two key properties: Head
which is the item at the head of the sequence, and Tail
which is the rest of the sequence. You can convert any existing collection type (as well as IEnumerable
types) to a Seq<A>
by calling the Seq
constructor:
var seq1 = Seq(List(1, 2, 3, 4, 5)); // Lst<A> -> Seq<A>
var seq2 = Seq(Arr(1, 2, 3, 4, 5)); // Arr<A> -> Seq<A>
var seq3 = Seq(new [] {1, 2, 3, 4, 5}); // A[] -> Seq<A>
...
As well as construct them directly:
var seq1 = Seq(1, 2, 3, 4, 5);
var seq2 = 1.Cons(2.Cons(3.Cons(4.Cons(5.Cons(Empty)))));
In practice if you're using Cons
you don't need to provide Empty
:
var seq = 1.Cons(); // Creates a sequence with one item in
The primary benefits are:
- Immutable
- Thread-safe
- Much lighter weight than using
Lst<A>
(althoughLst<A>
is very efficient, it is still an AVL tree behind the scenes) - Maintains the original type for
Lst<A>
,Arr<A>
, and arrays. So if you construct aSeq
from one of those types, theSeq
then gains extra powers for random lookups (Skip
), and length queries (Count
). - If you construct a
Seq
with anIEnumerable
then it maintains its laziness, but also guarantees that each item in the originalIEnumerable
is only ever enumerated once. - Obviously easier to type
Seq
thanIEnumerable
! Count
works by default for all non-IEnumerable
sources. If you construct with anIEnumerable
thenCount
will only cause a single evaluation of the underlyingIEnumerable
(and subsequent access to the seq for anything else doesn't cause additional evaluation)- Efficient pattern matching on the
Seq
, which again doesn't cause multiple evaluations of the underling collection.
Seq
has a bigger interface than IEnumerable
which allows for various bespoke optimisations depending on the underlying collection; which is especially powerful for the LINQ operators like Skip
, Take
, TakeWhile
,
public interface ISeq<A> : IEnumerable<A>, IEquatable<ISeq<A>>, IComparable<ISeq<A>>
{
/// <summary>
/// Head of the sequence
/// </summary>
A Head { get; }
/// <summary>
/// Head of the sequence
/// </summary>
Option<A> HeadOrNone();
/// <summary>
/// Tail of the sequence
/// </summary>
Seq<A> Tail { get; }
/// <summary>
/// True if this cons node is the Empty node
/// </summary>
bool IsEmpty { get; }
/// <summary>
/// Returns the number of items in the sequence
/// </summary>
/// <returns>Number of items in the sequence</returns>
int Count { get; }
/// <summary>
/// Match empty sequence, or multi-item sequence
/// </summary>
/// <typeparam name="B">Return value type</typeparam>
/// <param name="Empty">Match for an empty list</param>
/// <param name="Tail">Match for a non-empty</param>
/// <returns>Result of match function invoked</returns>
B Match<B>(
Func<B> Empty,
Func<A, Seq<A>, B> Tail);
/// <summary>
/// Match empty sequence, or one item sequence, or multi-item sequence
/// </summary>
/// <typeparam name="B">Return value type</typeparam>
/// <param name="Empty">Match for an empty list</param>
/// <param name="Tail">Match for a non-empty</param>
/// <returns>Result of match function invoked</returns>
B Match<B>(
Func<B> Empty,
Func<A, B> Head,
Func<A, Seq<A>, B> Tail);
/// <summary>
/// Match empty sequence, or multi-item sequence
/// </summary>
/// <typeparam name="B">Return value type</typeparam>
/// <param name="Empty">Match for an empty list</param>
/// <param name="Seq">Match for a non-empty</param>
/// <returns>Result of match function invoked</returns>
B Match<B>(
Func<B> Empty,
Func<Seq<A>, B> Seq);
/// <summary>
/// Match empty sequence, or one item sequence, or multi-item sequence
/// </summary>
/// <typeparam name="B">Return value type</typeparam>
/// <param name="Empty">Match for an empty list</param>
/// <param name="Tail">Match for a non-empty</param>
/// <returns>Result of match function invoked</returns>
B Match<B>(
Func<B> Empty,
Func<A, B> Head,
Func<Seq<A>, B> Tail);
/// <summary>
/// Map the sequence using the function provided
/// </summary>
/// <typeparam name="B"></typeparam>
/// <param name="f">Mapping function</param>
/// <returns>Mapped sequence</returns>
Seq<B> Map<B>(Func<A, B> f);
/// <summary>
/// Map the sequence using the function provided
/// </summary>
/// <typeparam name="B"></typeparam>
/// <param name="f">Mapping function</param>
/// <returns>Mapped sequence</returns>
Seq<B> Select<B>(Func<A, B> f);
/// <summary>
/// Filter the items in the sequence
/// </summary>
/// <param name="f">Predicate to apply to the items</param>
/// <returns>Filtered sequence</returns>
Seq<A> Filter(Func<A, bool> f);
/// <summary>
/// Filter the items in the sequence
/// </summary>
/// <param name="f">Predicate to apply to the items</param>
/// <returns>Filtered sequence</returns>
Seq<A> Where(Func<A, bool> f);
/// <summary>
/// Monadic bind (flatmap) of the sequence
/// </summary>
/// <typeparam name="B">Bound return value type</typeparam>
/// <param name="f">Bind function</param>
/// <returns>Flatmapped sequence</returns>
Seq<B> Bind<B>(Func<A, Seq<B>> f);
/// <summary>
/// Monadic bind (flatmap) of the sequence
/// </summary>
/// <typeparam name="B">Bound return value type</typeparam>
/// <param name="bind">Bind function</param>
/// <returns>Flatmapped sequence</returns>
Seq<C> SelectMany<B, C>(Func<A, Seq<B>> bind, Func<A, B, C> project);
/// <summary>
/// Fold the sequence from the first item to the last
/// </summary>
/// <typeparam name="S">State type</typeparam>
/// <param name="state">Initial state</param>
/// <param name="f">Fold function</param>
/// <returns>Aggregated state</returns>
S Fold<S>(S state, Func<S, A, S> f);
/// <summary>
/// Fold the sequence from the last item to the first
/// </summary>
/// <typeparam name="S">State type</typeparam>
/// <param name="state">Initial state</param>
/// <param name="f">Fold function</param>
/// <returns>Aggregated state</returns>
S FoldBack<S>(S state, Func<S, A, S> f);
/// <summary>
/// Returns true if the supplied predicate returns true for any
/// item in the sequence. False otherwise.
/// </summary>
/// <param name="f">Predicate to apply</param>
/// <returns>True if the supplied predicate returns true for any
/// item in the sequence. False otherwise.</returns>
bool Exists(Func<A, bool> f);
/// <summary>
/// Returns true if the supplied predicate returns true for all
/// items in the sequence. False otherwise. If there is an
/// empty sequence then true is returned.
/// </summary>
/// <param name="f">Predicate to apply</param>
/// <returns>True if the supplied predicate returns true for all
/// items in the sequence. False otherwise. If there is an
/// empty sequence then true is returned.</returns>
bool ForAll(Func<A, bool> f);
/// <summary>
/// Skip count items
/// </summary>
Seq<A> Skip(int count);
/// <summary>
/// Take count items
/// </summary>
Seq<A> Take(int count);
/// <summary>
/// Iterate the sequence, yielding items if they match the predicate
/// provided, and stopping as soon as one doesn't
/// </summary>
/// <returns>A new sequence with the first items that match the
/// predicate</returns>
Seq<A> TakeWhile(Func<A, bool> pred);
/// <summary>
/// Iterate the sequence, yielding items if they match the predicate
/// provided, and stopping as soon as one doesn't. An index value is
/// also provided to the predicate function.
/// </summary>
/// <returns>A new sequence with the first items that match the
/// predicate</returns>
Seq<A> TakeWhile(Func<A, int, bool> pred);
}
Breaking changes
- Previously there were functions in the
Prelude
calledseq
which took many types and turned them intoIEnumerable<A>
. Now they're constructingSeq<A>
I have renamed them toSeq
in line with other constructor functions. - Where sensible in the rest of the API I have changed
AsEnumerable()
to return aSeq<A>
. This is for types which are unlikely to have large sequences ...
Type-classes in C# - LanguageExt Version 2.0 beta
Version 2 (BETA) Release Notes
Version 2.0 of Language-Ext is now in beta. This is a major overhaul of every type in the system. I have also broken out the LanguageExt.Process
actor system into its own repo, it is now named Echo, so if you're using that you should head over to the repo and follow that. It's still in alpha at the moment, it's feature complete, it just needs more testing, so it's lagging behind at the moment.
Version 2.0 of Language-Ext actually just started out as a branch where I was trying out a new technique for doing ad-hoc polymorphism in C# (think somewhere between Haskell typeclasses and Scala implicits).
I didn't expect it to lead to an entire re-write. So a word of warning, there are many areas that I know will be breaking changes, but some I don't. Breaking changes will 99% of the time be compile time errors (rather than changes in behaviour that silently affect your code). So although I don't expect any major issues, I would put aside an afternoon to fix up any compilation breakages.
Often the breakages are for things like rectifying naming inconsistencies (for example some bi-map functions were named Map
, some named BiMap
, they're all now BiMap
), another example is that all collection types (Lst
, Map
, etc.) are now structs. So any code that does this will fail to compile:
Map<int, string> x = null;
The transformer extensions have been overhauled too (they provided overloads for nested monadic types, Option<Lst<A>>
for example). If you were cheating trying to get directly at values by calling Lift
or LiftUnsafe
, well, now you can't. It was a bad idea that was primarily to make the old transformer types work. So they're gone.
The overloads of Select
and SelectMany
are more restricted now, because combining different monadic types could lead to some type resolution issues with the compiler. You will now need to lift your types into the context of the LINQ expression (there are now lots of extensions to do that: ToOption
, ToTry
, etc.)
For the problems you will inevitablity have with upgrading to language-ext 2.0, you will also have an enormous amount of new benefits and possibilities.
My overriding goal with this library is to try and provide a safer environment in which to write C#. Version 1 was mostly trying to protect the programmer from null and mutable state. Version 2 is very much focussed on improving our lot when implementing abstract types.
Inheritance based polymorphism is pretty much accepted to be the worst performer in the polymorphic world. Our other option is parametric polymorphism (generics). With this release I have facilitated ad-hoc polymorphism with a little known technique in C#.
So for the first time it's possible to write numeric methods once for all numeric types or do structural equality testing that you can rely on.
Also there is support for the much more difficult higher-order polymorphic types like Monad<MA, A>
. LanguageExt 2.0 provides a fully type-safe and efficient approach to working with higher order types. So yes, you can now write functions that take monads, or functors, or applicatives, and return specialised values (rather than abstract or dynamic values). Instead of writing a function that takes an Option<A>
, you can write one that takes any monadic type, bind them, join them, map them, and return the concrete type that you pushed in.
Of course without compiler or runtime support for higher-order generics some hoops need to be jumped through (and I'm sure there will be some Haskell purist losing their shit over the approach). But at no point is the integrity of your types affected. Often the technique requires quite a large amount of generic argument typing, but if you want to write the super generic code, it's now possible. I don't know of any other library that provides this functionality.
This has allowed the transformer extensions to become more powerful too (because the code generator that emits them can now use the type-class/instance system). The Writer
monad can now work with any output type (as long as it has a Monoid
instance), so it's not limited to telling its output to an IEnumerable
, it can be a Lst
, a string
, an int
, or whatever Monoid
you specify.
Personally I find this very elegant and exciting. It has so much potential, but many will be put off by the amount of generic args typing they need to do. If anybody from the Rosyln team is reading this, please for the love of god help out with the issues around constraints and excessive specifying of generic arguments. The power is here, but needs more support.
Scroll down to the section on Ad-hoc polymorphism for more details.
Documentation
Full API documentation can be found here
Bug fixes
- Fix for
Lst.RemoveAt(index)
- certain tree arrangements caused this function to fail - Fix for
HSet
(nowHashSet
) constructor bug - constructing with an enumerable always failed
New features - LanguageExt.Core
New collection types:
Type | Description |
---|---|
HashSet<A> |
Ordering is done by GetHashCode() . Existence testing is with EqualityComparer<A>.Default.Equals(a,b) |
HashMap<A, B> |
Ordering is done by GetHashCode() . Existence testing is with EqualityComparer<A>.Default.Equals(a,b) |
HashSet<EqA, A> where EqA : struct, Eq<A> |
Ordering is done by GetHashCode() . Existence testing is with default(EqA).Equals(a,b) |
HashMap<EqA, A, B> |
Ordering is done by GetHashCode() . Existence testing is with default(EqA).Equals(a,b) |
Set<OrdA, A> where OrdA : struct, Ord<A> |
Ordering is done by default(OrdA).Compare(a,b) . Existence testing is with default(OrdA).Equals(a,b) |
Map<EqA, A, B> |
Ordering is done by default(OrdA).Compare(a,b) . Existence testing is with default(OrdA).Equals(a,b) |
Arr<A> |
Immutable array. Has the same access speed as the built-in array type, but with immutable cells. Modification is expensive, due to the entire array being copied per operation (although for very small arrays this would be more efficient than Lst<T> or Set<T> ). |
Lst<PredList, A> where PredList : struct, Pred<ListInfo> |
This allows lists to run a predicate on the Count property of the list after construction. |
Lst<PredList, PredItem, A> where PredItem : struct, Pred<A> |
This allows lists to run a predicate on the Count property of the list after construction and on items as they're being added to the list. |
As you can see above there are new type-safe key versions of Set
, HashSet
, Map
, and HashMap
. Imagine you want to sort the value of a set of strings in a case-insensitive way (without losing information by calling value.ToLower()
).
var map = Set<TStringOrdinalIgnoreCase, string>(...)
The resulting type would be incompatible with:
Set<TString, string>, or Set<TStringOrdinal, string>
And is therefore more type-safe than just using Set. Examples
The two new predicate versions of Lst
allow for properties of the list to travel with the type. So for example this shows how you can enforce a list to be non-empty:
public int Product(Lst<NonEmpty, int> list) =>
list.Fold(1, (s, x) => s * x);
There are implicit conversion operators between Lst<A>
and Lst<PredList, A>
, and between Lst<A>
and Lst<PredList, PredItem, A>
. They don't need to reallocate the collection, but converting to a more constrained type will cause the validation to run. This is very light for constructing Lst<PredList, A>
, but will cause every item in the list to be validated for Lst<PredList, PredItem, A>
.
And so it's possible to do this:
Lst<int> list = List<int>();
var res = Product(list); // ArgumentOutOfRangeException
That will throw an ArgumentOutOfRangeException
because the list is empty. Whereas this is fine:
Lst<int> list = List<int>(1, 2, 3, 4, 5);
var res = Product(list); // 120
To construct the predicate list types directly, call:
Lst<NonEmpty, int> list = List<NonEmpty, int>(1, 2, 3, 4, 5);
The second type of predicate Lst
is Lst<PredList, PredItem, A>
. PredItem
is a predicate that's run against every item being added to the list. Once the item is in the list it won't be checked again (because it's an immutable list).
For example, this is a Lst
that can't be empty and won't accept null
items.
var x = List<NonEmpty, NonNullItems<string>, string>("1", "2", "3");
Obviously declaring types like this gets quite bulky quite quickly. So only using them for method arguments is definitely a good approach:
public string Divify(Lst<NonEmpty, NonNullItems<string>, string> items) =>
String.Join(items.Map(x => $"<div>{x}</div>"));
Then Divify
can be invoked thus:
var res = Divify(List("1", "2", "3"));
// "<div>1</div><div>2</div><div>3</div>"
But as mentioned above, the implicit conversion from Lst<string>
to Lst<NonEmpty, NonNullItems<string>, string>
will run the NonNullItems<string>
predicate for each item in the Lst
.
Built-in are some standard Pred<ListInfo>
implementations:
AnySize
- Always succeedsCountRange<MIN, MAX>
- Limits theCount
to be >= MIN and <= MAXMaxCount<MAX>
- As above but with no lower boundNonEmpty
- List must have at l...
Language-ext 1.7 released!
Lots of things have been improved over the past 3 months, especially in the Process system, but in general also. Now feels like a good time to get a release out. Any problems, shout!
Core
- Added: Units of measure:
Length
,Velocity
,Acceleation
,Area
andTime
- Added:
List.span
- applied to a predicate and a list, returns a tuple where first element is longest prefix of elements that satisfy the predicate and second element is the remainder of the list. - Added:
List.tails
function - returns all final segments of the list argument, longest first. - Added: Applicative support for
Option
,OptionUnsafe
,Either
,EitherUnsafe
,Try
,TryOption
,List
,IEnumerable
(Prelude.apply
function orApply
extension method) - Added: Support for arithmetic operators on all monadic types with provision of
IAppendable
,ISubtractable
,IProductable
,IDivisible
andINumeric
. Allowing for operations like this:Some(10) + Some(10) == Some(30)
, andList(1,2,3) + List(4,5,6) == List(1,2,3,4,5,6)
- Added:
List.foldUntil
,List.foldWhile
- Added: Functional version of
using(...) { }
calleduse
- Added:
tryuse
which is a version of 'use' that returns aTry<T>
; whereT
is anIDisposable
- Added: Extension method:
Try<T>.Use()
, same astryuse
- Added:
Try<T>.IfFail(Exception ex => ...)
- Added:
Try<T>.IfFail().Match()
- allows matching ofException
types as a single expression
Tuple<A,B>
bi-functor, bi-foldable, bi-iterable
Tuple<A,B,C>
tri-functor, tri-foldable, tri-iterable - Added:
Serializable
attribute toEither
,EitherUnsafe
,Lst
,ListItem
,Map
,MapItem
,Option
,OptionUnsafe
,Que
,Stck
, - Moved
Prelude.init
,Prelude.initInfinite
,Prelude.repeat
toList
Process system
- Strategy system - Decalarative way of dealing with Process failure
- Match on exceptions and map to Directives (Stop, Restart, Resume, Escalate)
- Map the Directives to MessageDirectives to decided on the fate of the failed message (Send to dead letters, Send to self (front or back of the queue), Send to supervisor, Send to specified Process)
- Session system - Allows consuming code to set a session-ID that is propagated throughout the system with the messages. Very useful for hooking up to authentication systems.
- Roles. Each node in a cluster must specify a role name.
- Process.ClusterNodes property - Has a map of alive cluster nodes and their roles.
- Routers - Processes that can auto-route messages to child processes or a provided set of processes. Default routers include:
- Broadcast
- Round-robin
- Random
- Least-busy
- Dispatchers - Like routers but without an actual Process. Dispatchers have a sender specified list of Processes to dispatch to, i.e. Dispatch.leastBusy(pid1, pid2, pid3). There are four default dispatchers:
- Broadcast
- Round-robin
- Random
- Least-busy
Bespoke dispacthers can be registered using Dispatch.register(...)
- Role dispatchers - Using a combination of
Process.ClusterNodes
and dispatchers, the Role dispatchers allow for ProcessIds to be built that refer to locations in the cluster by role. ie.Role.Broadcast["mail-server"]["user"]["smtp"]
will create aProcessId
that looks like this:/disp/role-broadcast/user/smtp
. Because it's aProcessId
it can be used with any of the existing functionality that accepts ProcessIds (tell
,ask
,subscribe
, etc.) but it has a behaviour baked in (broadcast
in this case). So doing atell
will send to allsmtp
Processes in the cluster.
There are severalRole
dispatchers:Role.Broadcast
Role.RoundRobin
Role.Random
Role.LeastBusy
Role.First
Role.Second
Role.Third
Role.Last
- Death-watch system:
Process.watch
andProcess.unwatch
to get oneProcess
watching for the death of another. Separate inbox calledTerminated
is used so that your primary inboxes can stay strongly typed. Process
setup functions are invoked immediately, rather than on their first message.- More improvements to the F# API to bring it closer to parity with the C# API - this work is on-going.
- Process.js now has a Process.spawnView function (if knockout.js is in use) that allows for synchronisation of
Process
state and view state as well as hooking up event functions.
Breaking changes.
Hopefully very few. But areas to watch out for:
- Remove the old
with
functions that were deprecated a while ago because I needed them for a newmatch
by value and exception function. If you get errors around this area, you should usePrelude.map
instead. Cluster.connect
now expects an extra parameter. It is the role of the node in the cluster. You don't have to use roles, but you do have to give it a validProcessName
- F# Process system has had its Process ID type changed from
(unit -> ProcessId)
toProcessId
. It was a useful experiment for a while to avoid makingProcessFs.Self
andProcessFs.Sender
into functions; but has created friction between the C# and F# implementations ever since. So I have changed the F# API to use the sameProcessId
that the C# does and have changedProcessFs.Self
and the like to return 'special' ProcessIds (/__special__/self
for example) that are resolved on use. So you should also be careful if you ever need to send these special paths around to useresolvePID pid
to extract the actual path. This does mean thattell (Sender()) msg (Self())
can be writtentell Sender msg Self
, which I think is much more attractive and easy to deal with. So the occassionalresolvePID Self
I think is less of a problem.
Documentation
- There's more of it!
Thanks to @la-yumba and @Jagged for their help with this release.
Nu-get
As usual everything is release on NuGet:
- LanguageExt.Core - https://www.nuget.org/packages/LanguageExt/
- LanguageExt.Process - https://www.nuget.org/packages/LanguageExt.Process
- LanguageExt.Process.Redis - https://www.nuget.org/packages/LanguageExt.Process.Redis
Remove dependency on System.Collections.Immutable
Removed dependency on System.Collections.Immutable
. All immutable types are implemented in-project:
Lst<T>
Map<K,V>
Set<T>
Que<T>
Stck<T>
Either
and EitherUnsafe
API rebalancing (between Left and Right behaviours). Also Either
and EitherUnsafe
are now bifunctors, bifoldable and bitraversible.
Compose
and BackCompose
extension methods for Func<A,B>
. Also, new compose
function in the Prelude
NuGet:
Language-Ext Version 1.5 release
This is a major new release of Language-Ext with myriad new features and improvements. Much of this release is about refining and standardising the API. This is the first stable release of the LanguageExt.Process
system (Actor system), LanguageExt.Process.FSharp
(F# API for LanguageExt.Process
), LanguageExt.ProcessJS
(Javascript Actor system that allows seamless actor messaging from server to client), and LanguageExt.Process.Redis
(for intra-system messaging and message/state persistence).
Standardisation comes in the form:
- Making all constructor functions start with a capital letter
- Making all fold and scan functions use the same fold function signature
- Creating a common set of functions that all of the monad types implement
So:
tuple
becomesTuple
list
becomesList
map
becomesMap
cons
becomesCons
- etc.
This makes the constructor functions consistent with Some
, None
, Left
, Right
, ... The only exception to this is the unit
constructor, which stays as-is.
We also bring in a concept of a type of a 'higher kinded type' for all of the monadic types. It's not quite as effective or refined as a Haskell HKT, but it does create a standard interface (through extension methods) for all of the monadic types. The standard interface includes the following functions:
Sum
Count
Bind
Exists
Filter
Fold
ForAll
Iter
Map
Lift
/LiftUnsafe
SelectMany
Select
Where
Because of this standardised interface, it's also possible to use monad transformer functions (for up to a two-level deep generic monad-type):
SumT
BindT
CountT
ExistsT
FilterT
FoldT
ForAllT
IterT
MapT
i.e.
var list = List(Some(1), None, Some(2), None, Some(3)); // Lst<Option<int>>
var presum = list.SumT(); // 6
list = list.MapT( x => x * 2 );
var postsum = list.SumT(); // 12
New types
Map<K,V>
- Replacement forImmutableDictionary<K,V>
that uses an AVL tree implementationLst<T>
- Wrapper forImmutableList<T>
Set<T>
- Wrapper forImmutableSet<T>
Try<T>
- LikeTryOption<T>
but without the optional return valueRWS<E,W,S>
- Reader/Writer/State monadExceptionMatch
- Used for newException
extension methodMatch
for pattern matching on exception types.ActionObservable<T>
- Used byIObservable<T>
extension methodPostSubscribe
. Allows an action to be executed post-subscription.
New functions
- static Process functions in
LanguageExt.Process
par
- Partial applicationcurry
- Curryingrandom
- Thread safe random number generatorifSome
added forTryOption
- dispatches an action if theTryOption
is in aSome
state
Improvements
- Improved
Unit
type - ImplementedIEquatable<Unit>
and equality operator overloads - Improved
IComparable
andIEquatable
support forOption
andEither
types
Breaking changes
- Standardised the
fold
andscan
delegates toFunc<S,T,S>
Deprecated (made obsolete)
tuple
- becomesTuple
query
- becomesQuery
map
- becomesMap
list
- becomesList
array
- becomesArray
stack
- becomesStack
failure
- becomesifNone
/ifLeft
Prelude.empty
- becomesList.empty
/Set.empty
/Map.empty
cons
- becomesCons
range
- becomesRange
with
- becomesmap
Nuget:
Language-ext 1.0.0
Version 1.0.0
Now that System.Collections.Immutable
has been fully released (as version 1.1.36) we can now do a full release of Language-Ext.
As usual you can find the package on NuGet
Language-ext
Use and abuse the features of C#, which, if you squint, can look like extensions to the language itself. This package is a functional 'toolkit' and also solves some of the annoyances with C#, namely:
- Poor tuple support
- Null reference problem
- Lack of lambda and expression inference
- Void isn't a real type
- Mutable lists, dictionaries, sets, queues, etc.
- The awful 'out' parameter
- Common core functional types missing (
Option
,Either
,Unit
, ...)
The library very heavily focusses on correctness, to give you the tools needed to write safe declarative code.
Features:
Powerful 'prelude' which you include by using static LanguageExt.Prelude
(in C# 6) that covers many of the basic functional language core library functions and types (from using LanguageExt
):
- Pattern matching
- Lambda type-inference:
var fn = fun( (int x, int y) => x + y );
Option<T>
,OptionUnsafe<T>
,Either<L,R>
,EitherUnsafe<L,R>
andTryOption<T>
monads (probably the most complete implementations you'll find in the .NET world)tuple(a,b,...)
-Tuple
construction without typingTuple.Create(a,b,...)
as well asmap
to project theItem1..ItemN
properties onto named values.List
- immutable listMap
- immutable mapSet
- immutable setmemo
- Memoization with auto-cache purging using weak-referencesWriter
monadReader
monadState
monad- Extension methods and replacement functions for dealing with
out
(Int32.TryParse
,IDictionary.TryGetValue
, etc.)
This only skims the surface of the library
Release notes
Additions:
- New
Reader<E,T>
monadPrelude.Reader
constructor functionPrelude.ask
functionPrelude.local
function
- New
Writer<W,T>
monadPrelude.Writer
constructor functionPrelude.tell
function
- New
State<S,T>
monadPrelude.State
constructor functionPrelude.get
functionPrelude.put
function
Option<T>
IfSome
method for dispatching actions and ignoringNone
Prelude.ifSome
as aboveIfNone
method (replacesFailure
)Prelude.ifNone
function (replacesPrelude.failure
)ToEither
converts anOption<T>
to anEither<L,R>
(you must provide a default(L) value or func incase theOption
is in aNone
state)ToEitherUnsafe
converts anOption<T>
to anEitherUnsafe<L,R>
(you must provide a default(L) value or func incase the Option is in a None state)Some
fluent method now also supportsAction
*OptionUnsafe<T>
IfSomeUnsafe
method for dispatching actions and ignoringNone
Prelude.ifSomeUnsafe
as aboveIfNoneUnsafe
method (replacesFailureUnsafe
)Prelude.ifNoneUnsafe
function (replacesPrelude.failureUnsafe
)ToEitherUnsafe
converts anOptionUnsafe<T>
to anEitherUnsafe<L,R>
(you must provide a default(L) value or func incase the OptionUnsafe is in a None state)Some
fluent method now also supportsAction
TryOption<T>
IfSome
method for dispatching actions and ignoringNone
orFail
Prelude.ifSome
as aboveIfNone
method (replacesFailure
)Prelude.ifNone
function (replacesPrelude.failure
)IfNoneOrFail
method for handling both failure states separately (Some state uses identity function)Prelude.ifNoneOrFail
as aboveTryOptionConfig.ErrorLogger
static variable which can be used to attach error logging behaviour to theFail
state ofTryOption
Prelude.tryfun
function wraps aTryOption
in aFunc
ToOption
converts aTryOption<T>
to aOption<T>
(Fail becomes None)Some
fluent method now also supportsAction
Either<L,R>
IfRight
method for dispatching actions and ignoringLeft
Prelude.ifRight
as aboveIfLeft
method (replacesFailure
)Prelude.ifLeft
method (replacesPrelude.failure
)Right
fluent method now also supportsAction
ToOption
converts anEither<L,R>
to anOption<R>
(L
becomesNone
)ToTryOption
converts anEither<L,R>
to aTryOption<R>
ToEitherUnsafe
converts anEither<L,R>
to anEitherUnsafe<L,R>
(L
becomesNone
)
EitherUnsafe<L,R>
IfRightUnsafe
method for dispatching actions and ignoringLeft
Prelude.ifRightUnsafe
as aboveIfLeftUnsafe
method (replacesFailureUnsafe
)Prelude.ifLeftUnsafe
method (replacesPrelude.failureUnsafe
)Right
fluent method now also supportsAction
Updates:
Prelude.convert<T>
now returnsNone
if input isnull
(it previously threw an exception)
Fixes:
- Query.zip would go into an infinite loop. Fixed.
- Comments
Deprecated:
- Dependency on
ConcurrentHashTable
failure
andFailure
(forifNone
,IfNone
,ifLeft
, etc.)Iter
extension method inQuery
, it was causing resolution problems for the compiler.- Removed
RightUnsafe
andLeftUnsafe
fromEither
, these were a hangover from whenEitherUnsafe
didn't exist andEither
had a dual role. This isn't needed any more.