Skip to content

Releases: louthy/language-ext

Record types in C# !

30 Jul 20:45
Compare
Choose a tag to compare

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 use NonSerializable)

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

15 Jun 23:18
Compare
Choose a tag to compare

Either updates

  • Partition to use ValueTuple
  • Partition/Lefts/Rights Seq variants
  • Moved Typeclass attribute to LanguageExt.Attributes

All on nu-get:

Breaking change: Flipped Compose and BackCompose

17 May 12:29
Compare
Choose a tag to compare

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

21 Apr 17:29
Compare
Choose a tag to compare

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 (now HashSet) 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>"...
Read more

Seq - a better IEnumerable

20 Apr 15:31
Compare
Choose a tag to compare
Pre-release

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> (although Lst<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 a Seq from one of those types, the Seq then gains extra powers for random lookups (Skip), and length queries (Count).
  • If you construct a Seq with an IEnumerable then it maintains its laziness, but also guarantees that each item in the original IEnumerable is only ever enumerated once.
  • Obviously easier to type Seq than IEnumerable!
  • Count works by default for all non-IEnumerable sources. If you construct with an IEnumerable then Count will only cause a single evaluation of the underlying IEnumerable (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 called seq which took many types and turned them into IEnumerable<A>. Now they're constructing Seq<A> I have renamed them to Seq in line with other constructor functions.
  • Where sensible in the rest of the API I have changed AsEnumerable() to return a Seq<A>. This is for types which are unlikely to have large sequences ...
Read more

Type-classes in C# - LanguageExt Version 2.0 beta

06 Mar 04:51
Compare
Choose a tag to compare

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 (now HashSet) 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 succeeds
  • CountRange<MIN, MAX> - Limits the Count to be >= MIN and <= MAX
  • MaxCount<MAX> - As above but with no lower bound
  • NonEmpty - List must have at l...
Read more

Language-ext 1.7 released!

08 Dec 00:55
Compare
Choose a tag to compare

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 and Time
  • 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 or Apply extension method)
  • Added: Support for arithmetic operators on all monadic types with provision of IAppendable, ISubtractable, IProductable, IDivisible and INumeric. Allowing for operations like this: Some(10) + Some(10) == Some(30), and List(1,2,3) + List(4,5,6) == List(1,2,3,4,5,6)
  • Added: List.foldUntil, List.foldWhile
  • Added: Functional version of using(...) { } called use
  • Added: tryuse which is a version of 'use' that returns a Try<T>; where T is an IDisposable
  • Added: Extension method: Try<T>.Use(), same as tryuse
  • Added: Try<T>.IfFail(Exception ex => ...)
  • Added: Try<T>.IfFail().Match() - allows matching of Exception 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 to Either, EitherUnsafe, Lst, ListItem, Map, MapItem, Option, OptionUnsafe, Que, Stck,
  • Moved Prelude.init, Prelude.initInfinite, Prelude.repeat to List

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 a ProcessId that looks like this: /disp/role-broadcast/user/smtp. Because it's a ProcessId 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 a tell will send to all smtp Processes in the cluster.
    There are several Role dispatchers:
    • Role.Broadcast
    • Role.RoundRobin
    • Role.Random
    • Role.LeastBusy
    • Role.First
    • Role.Second
    • Role.Third
    • Role.Last
  • Death-watch system: Process.watch and Process.unwatch to get one Process watching for the death of another. Separate inbox called Terminated 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 new match by value and exception function. If you get errors around this area, you should use Prelude.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 valid ProcessName
  • F# Process system has had its Process ID type changed from (unit -> ProcessId) to ProcessId. It was a useful experiment for a while to avoid making ProcessFs.Self and ProcessFs.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 same ProcessId that the C# does and have changed ProcessFs.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 use resolvePID pid to extract the actual path. This does mean that tell (Sender()) msg (Self()) can be written tell Sender msg Self, which I think is much more attractive and easy to deal with. So the occassional resolvePID 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:

Remove dependency on System.Collections.Immutable

04 Oct 21:16
Compare
Choose a tag to compare

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

22 Sep 23:17
Compare
Choose a tag to compare

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 becomes Tuple
  • list becomes List
  • map becomes Map
  • cons becomes Cons
  • 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 for ImmutableDictionary<K,V> that uses an AVL tree implementation
  • Lst<T> - Wrapper for ImmutableList<T>
  • Set<T> - Wrapper for ImmutableSet<T>
  • Try<T> - Like TryOption<T> but without the optional return value
  • RWS<E,W,S> - Reader/Writer/State monad
  • ExceptionMatch - Used for new Exception extension method Match for pattern matching on exception types.
  • ActionObservable<T> - Used by IObservable<T> extension method PostSubscribe. Allows an action to be executed post-subscription.

New functions

  • static Process functions in LanguageExt.Process
  • par - Partial application
  • curry - Currying
  • random - Thread safe random number generator
  • ifSome added for TryOption - dispatches an action if the TryOption is in a Some state

Improvements

  • Improved Unit type - Implemented IEquatable<Unit> and equality operator overloads
  • Improved IComparable and IEquatable support for Option and Either types

Breaking changes

  • Standardised the fold and scan delegates to Func<S,T,S>

Deprecated (made obsolete)

  • tuple - becomes Tuple
  • query - becomes Query
  • map - becomes Map
  • list - becomes List
  • array - becomes Array
  • stack - becomes Stack
  • failure - becomes ifNone / ifLeft
  • Prelude.empty - becomes List.empty / Set.empty / Map.empty
  • cons - becomes Cons
  • range - becomes Range
  • with - becomes map

Nuget:

Language-ext 1.0.0

11 Jun 20:59
Compare
Choose a tag to compare

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> and TryOption<T> monads (probably the most complete implementations you'll find in the .NET world)
  • tuple(a,b,...) - Tuple construction without typing Tuple.Create(a,b,...) as well as map to project the Item1..ItemN properties onto named values.
  • List - immutable list
  • Map - immutable map
  • Set - immutable set
  • memo - Memoization with auto-cache purging using weak-references
  • Writer monad
  • Reader monad
  • State 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> monad
    • Prelude.Reader constructor function
    • Prelude.ask function
    • Prelude.local function
  • New Writer<W,T> monad
    • Prelude.Writer constructor function
    • Prelude.tell function
  • New State<S,T> monad
    • Prelude.State constructor function
    • Prelude.get function
    • Prelude.put function
  • Option<T>
    • IfSome method for dispatching actions and ignoring None
    • Prelude.ifSome as above
    • IfNone method (replaces Failure)
    • Prelude.ifNone function (replaces Prelude.failure)
    • ToEither converts an Option<T> to an Either<L,R> (you must provide a default(L) value or func incase the Option is in a None state)
    • ToEitherUnsafe converts an Option<T> to an EitherUnsafe<L,R> (you must provide a default(L) value or func incase the Option is in a None state)
    • Some fluent method now also supports Action
      *OptionUnsafe<T>
    • IfSomeUnsafe method for dispatching actions and ignoring None
    • Prelude.ifSomeUnsafe as above
    • IfNoneUnsafe method (replaces FailureUnsafe)
    • Prelude.ifNoneUnsafe function (replaces Prelude.failureUnsafe)
    • ToEitherUnsafe converts an OptionUnsafe<T> to an EitherUnsafe<L,R> (you must provide a default(L) value or func incase the OptionUnsafe is in a None state)
    • Some fluent method now also supports Action
  • TryOption<T>
    • IfSome method for dispatching actions and ignoring None or Fail
    • Prelude.ifSome as above
    • IfNone method (replaces Failure)
    • Prelude.ifNone function (replaces Prelude.failure)
    • IfNoneOrFail method for handling both failure states separately (Some state uses identity function)
    • Prelude.ifNoneOrFail as above
    • TryOptionConfig.ErrorLogger static variable which can be used to attach error logging behaviour to the Fail state of TryOption
    • Prelude.tryfun function wraps a TryOption in a Func
    • ToOption converts a TryOption<T> to a Option<T> (Fail becomes None)
    • Some fluent method now also supports Action
  • Either<L,R>
    • IfRight method for dispatching actions and ignoring Left
    • Prelude.ifRight as above
    • IfLeft method (replaces Failure)
    • Prelude.ifLeft method (replaces Prelude.failure)
    • Right fluent method now also supports Action
    • ToOption converts an Either&lt;L,R&gt; to an Option&lt;R&gt; (L becomes None)
    • ToTryOption converts an Either&lt;L,R&gt; to a TryOption&lt;R&gt;
    • ToEitherUnsafe converts an Either&lt;L,R&gt; to an EitherUnsafe&lt;L,R&gt; (L becomes None)
  • EitherUnsafe<L,R>
    • IfRightUnsafe method for dispatching actions and ignoring Left
    • Prelude.ifRightUnsafe as above
    • IfLeftUnsafe method (replaces FailureUnsafe)
    • Prelude.ifLeftUnsafe method (replaces Prelude.failureUnsafe)
    • Right fluent method now also supports Action

Updates:

  • Prelude.convert<T> now returns None if input is null (it previously threw an exception)

Fixes:

  • Query.zip would go into an infinite loop. Fixed.
  • Comments

Deprecated:

  • Dependency on ConcurrentHashTable
  • failure and Failure (for ifNone, IfNone, ifLeft, etc.)
  • Iter extension method in Query, it was causing resolution problems for the compiler.
  • Removed RightUnsafe and LeftUnsafe from Either, these were a hangover from when EitherUnsafe didn't exist and Either had a dual role. This isn't needed any more.

https://www.nuget.org/packages/LanguageExt/