Version 2.0 of Language-Ext is now in released. 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.
Full API documentation can be found here
- 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
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>"));
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 least one item
And by default there are lots of Pred<A>
implementations. See the NewType
discussion later.
A new feature 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#). It's a singly-linked list, with 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 (likeOption
,Either
,Try
, etc.); You may find casting issues in those situations, but the types returned are obviously more useful, so it feels like a win. Let me know if you have issues (All the major types have aToSeq()
method where appropriate, for convenience). - In
LanguageExt.Parsec
any parsers that previously returnedIEnumerable<A>
now returnSeq<A>
; I noticed when using theParsec
library extensively that I would often useToArray()
orFreeze()
to force strict evaluation of aIEnumerable
result. Now you don't have to, but you may see some casting issues. Match
andmatch
forIEnumerable
previously allowed for matching up to six items in six separate lambdas. They're now gone because I doubt anybody uses them, and they're slightly unwieldy. Now they useSeq
behind the scenes to remove any multiple evaluations during the matching process:
static int Sum(Seq<int> seq) =>
seq.Match(
() => 0,
x => x,
(x, xs) => x + Sum(xs));
In the ongoing quest to make it safer to write C# code, these types are all now structs and therefore can't be null
:
Stck<A>,
Que<A>,
Lst<A>,
Map<A, B>,
Map<Ord, A, B>,
HashMap<A, B>,
HashMap<Eq, A, B>,
Set<A>,
Set<Ord, A>
HashSet<A, B>,
HashSet<Eq, A, B>,
Que<A>,
Arr<A>
This means you can create a member field and not initialise it and everything will 'just work':
static class Test
{
public static Map<string, int> Foo;
}
Assert.True(Test.Foo == Map.empty<string, int>());
Assert.True(Test.Foo == default(Map<string, int>);
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
Option<A>
and OptionUnsafe<A>
have until now been strict types. They can now support both strict and lazy behaviour in the same type.
For example:
var option = from w in Some(_ => 5)
from x in Some(_ => 10)
from y in Some(_ => 15)
from z in Some(_ => 20)
select w + x + y + z;
// At this point the w + x + y + z expression hasn't run
Any function that returns a concrete non-Option type will force the expression to be evaluated:
var result = option.IfNone(0); // 50
The result is memoised and therefore subsequent calls to Match
or similar won't re-evaluate the expression. The strict behaviour is unaltered:
var option = from w in Some(5)
from x in Some(10)
from y in Some(15)
from z in Some(20)
select w + x + y + z;
// At this point the w + x + y + z expression *has* run
If strict and lazy Options are combined, then the whole expression becomes lazy:
var option = from w in Some(5)
from x in Some(_ => 10) // lazy expression
from y in Some(15)
from z in Some(20)
select w + x + y + z;
// At this point the w + x + y + z expression hasn't run
Note if you want to write functions that return lazy options, then you will need to wrap them in Optional
or Some
:
Option<int> LazyOptionFoo() => Optional(_ => ... );
Either and EitherUnsafe will have lazy functionality soon
NewType has gained an extra generic argument. So this:
class Metres : NewType<double> { ... }
Becomes:
class Metres : NewType<Metres, double> { ... }
That makes lots of functionality more type-safe for NewType
derived types. For example monadic and functor operators like Select
, Map
, Bind
, SelectMany
can now return Metres
rather than NewType<double>
. Which is very important for the integrity of the type.
There is a variant that takes an additional generic argument PRED
. Which is constrained to be a struct
of Pred<A>
. This is called in the base constructor:
if (!default(PRED).True(value))
throw new ArgumentOutOfRangeException(nameof(value), value);
So you should be able to see that this allows validation to be embedded into the type. Here's an example from the new Process system client code:
public class ClientConnectionId : NewType<ClientConnectionId, string, StrLen<I10, I100>>
{
public ClientConnectionId(string value) : base(value)
{ }
}
ClientConnectionId
is like a session token, and StrLen<I10, I100>
means the string must be 10
to 100
chars long. It is defined thus:
public struct StrLen<NMin, NMax> : Pred<string>
where NMin : struct, Const<int>
where NMax : struct, Const<int>
{
[Pure]
public bool True(string value) =>
Range<int, TInt, NMin, NMax>.Is.True(value?.Length ?? 0);
}
NMin
and NMax
are constrained to be Const<int>
, and from the example above are I10
and I100
. They're defined as:
public struct I10 : Const<int> { public int Value => 10; }
public struct I100 : Const<int> { public int Value => 100; }
You can define your own struct
type that derives from Pred<A>
to provide any common validation primatives that you like. And defined constants by deriving from Const<A>
.
These are the built in predicate types:
True<A>
- Always succeeds, no matter what's passedFalse<A>
- Always fails, no matter what's passedEqual<A, EQ, CONST>
- ValueA
must be equal toCONST
.EQ
is aEq<A>
instanceExists<A, Term1, ..., Term5>
-Term1 - Term5
are predicates. The test passes ifA
is true when applied to any of the terms.ForAll<A, Term1, ..., Term5>
-Term1 - Term5
are predicates. The test passes ifA
is true when applied to all of the terms.GreaterThan<A, ORD, CONST>
- ValueA
must be greater thanCONST
.ORD
is anOrd<A>
instance.GreaterOrEq<A, ORD, CONST>
- ValueA
must be greater than or equal toCONST
.ORD
is anOrd<A>
instance.LessThan<A, ORD, CONST>
- ValueA
must be less thanCONST
.ORD
is anOrd<A>
instance.LessOrEq<A, ORD, CONST>
- ValueA
must be less than or equal toCONST
.ORD
is anOrd<A>
instance.Range<A, ORD, MIN, MAX>
- ValueA
must be in the rangeMIN
toMAX
.ORD
is anOrd<A>
instance.
Constants are in the LanguageExt.ClassInstances.Const
namespace. Integers are prefixed with I
; the most commonly used are already created: 0
to 256
, then powers of two and hundreds, thousands, etc. Double
constants are prefixed with D
. Only D0
, D1
, and DNeg1
exist. Char
constants are 'A'- 'Z'
, 'a' - 'z'
, '0' - '9'
, ChSpace
, ChTab
, ChCR
, ChLF
.
By embedding the validation into the type, there is no 'get out of jail free' cards where a loophole can be found in the type (or sub-type). And it also becomes fundamentally a different type to (for example) NewType<ClientConnectionId, string, StrLen<I0, I10>>
(See the <I0, I10>
at the end); so any function that wants to work with a client connection token must accept either ClientConnectionId
or its base type of NewType<ClientConnectionId, string, StrLen<I10, I100>>
. It gives a small glimpse into the world of dependently typed languages, I think this is a pretty powerful concept for improving the safety of C# types in general (with no runtime costs), and takes the original NewType
idea to the next level.
There is now an explicit cast operator. So for the Metres
example above:
Metres m = Metres.New(100);
double x = (double)m * 2.0;
With the new type-classes and class-instances (see later), it's now possible to write generic code for numeric-types. And so I have created a variant of NewType
called NumType
. Numeric types like int
are the kind of types that are very commonly made into NewType
derived types (along with string
), but previously there wasn't a good story for doing arithmetic on those types. Now with the NumType
it is possible. They work in exactly the same way as NewTypes
, but you must specify a Num<A>
class-instance (below it's TDouble
):
public class Metres : NumType<Metres, TDouble, double> {
public Metres(double x) : base(x) {}
}
That gives you these extras over NewType
:
operator+
operator*
operator/
operator-
Product()
Divide()
Plus()
Subtract()
Abs()
Signum()
Min()
Max()
Sum()
As with NewType
you can also use a predicate:
public class Age : NumType<Age, TInt, int, Range<int, TInt, I0, I120>>
{
public Age(int x) : base(x) {}
}
Even more specialised than NumType
and NewType
in that it only accepts class-instances from the type-class Floating<A>
. It adds functionality that are only useful with floating-point number types (along with the functionality from NumType
and NewType
):
Exp(), Sqrt(), Log(), Pow(A exp), LogBase(A y), Sin(), Cos(), Asin(), Acos(), Atan(), Sinh()
Cosh(), Tanh(), Asinh(), Acosh(), Atanh()
A new feature of C# 7 is syntax for tuples. Instead of:
Tuple.Create(a, b)
You can now:
(a, b)
You can also give them names:
(K Key, V Value)
So wherever in lang-ext tuples are used, they now accept ValueTuple
. ValueTuple
is the struct
version of Tuple
that C# is using to back the new syntax.
This is particularly nice for Map
:
var map = Map(
(1, "Paul"),
(2, "Steve"),
(3, "Stan"),
(4, "Tanzeel"),
(5, "Dan"),
(6, "Andreas"));
Also Map
now derives from IEnumerable<(K Key, V Value)>
.
Tuples look like they're going to be a lot more important in C# going forwards, so I have created extension methods and Prelude
functions for Tuple
and ValueTuple
with up to 7 items.
// Add an item to a tuple
var (a,b,c) = (1, 2).Add(3);
var (a,b,c,d) = (1, 2).Add(3).Add("Hello");
If your tuple contains Semigroups (like Lst
and int
for example) then you can call Append
:
var (a, b) = append<TLst<int>, TInt, Lst<int>, int>(
(List(1,2,3), 3),
(List(4,5,6,7), 4));
// ([1,2,3,4,5,6,7], 7)
Or:
var list = append<TLst<int>, Lst<int>>( (List(1,2,3), List(4,5,6,7)) );
// [1,2,3,4,5,6,7]
Head
and Tail
:
var a = ("a", 123, true).Head(); // "a"
var bc = ("a", 123, true).Tail(); // (123, true)
Sum
and Product
:
var a = (100, 200, 300).Sum<TInt, int>(); // 600
var b = (10, 10, 10).Product<TInt, int>(); // 1000
Contains
:
var a = (1,2,3,4,5).Contains<EqInt, int>(3); // true
Mapping:
x = x.Map( tuple => tuple );
x = x.BiMap(a => a * 2, b => b * 3);
x = x.TriMap(a => a * 2, b => b * 3, c => c + " add");
x = x.QuadMap(a => a * 2, b => b * 3, c => "over", d => "ride");
// etc.
x = x.MapFirst(a => a * 2); // just maps the first item and leaves the rest alone
x = x.MapSecond(b => b * 3);
x = x.MapThird(c => c + " add");
x = x.MapFourth(d => "change");
// etc.
var (a, b) = (100, "text").MapFirst(x => x * 2); // (200, "text")
Also:
Iter, Fold, BiFold, BiFoldBack, TriFold, TriFoldBack, etc.
Cond
allows for building conditional expressions that can be used fluently. It also seamlessly steps between synchronous and asynchronous behaviour without any need for ceremony.
Here's a simple example:
var cond = Cond<int>(x => x == 4)
.Then(true)
.Else(false);
That can be run like so:
bool result = cond(4); // True
bool result = cond(0); // False
Or,
bool result = 4.Apply(cond); // True
bool result = 0.Apply(cond); // False
Here's a slightly more complex example:
var vowels = Subj<char>().Map(Char.ToLower)
.Any(x => x == 'a', x => x == 'e', x => x == 'i', x => x == 'o', x => x == 'u')
.Then("Is a vowel")
.Else("Is a consonant");
var x = vowels('a'); // "Is a vowel"
This can then be tagged onto anything that returns a char or a Task<char>
:
var res = GetCharFromRemoteServer().Apply(vowels); // Task<string>
See the pull request for the discussion that led to this feature. Thanks to @ncthbrt for the suggestion and initial implementation.
Continuing the super generic theme, there is now a new Range
type that can handle any type (as long as they have Monoid
and Ord
instances):
public class Range<SELF, MonoidOrdA, A> : IEnumerable<A>
where SELF : Range<SELF, MonoidOrdA, A>
where MonoidOrdA : struct, Monoid<A>, Ord<A>, Arithmetic<A>
{
...
}
As with NewType
, FloatType
, and NumType
, anything that derives from it should provide itself as the first generic argument. This is the definition of IntegerRange
:
public class IntegerRange : Range<IntegerRange, TInt, int>
{
IntegerRange(int min, int max, int step) : base(min, max, step) { }
}
Everything else is handled in the base class, so it's trivial to add your own. As before they implement IEnumerable<A>
, and are lazy. They now support Overlaps(SELF range)
and InRange(A x)
. There are two constructor functions: Range.FromMinMax(min, max, step)
and Range.FromCount(min, count, step)
. There are several provided implementations: BigIntegerRange
, CharRange
, DecimalRange
, DoubleRange
, FloatRange
, IntegerRange
, LongRange
, ShortRange
.
Try
and TryOption
(the lazy monads that catch exceptions) have been improved to memoise everything they do. So once you run a Try
, running the same reference again won't re-invoke the computation, it will just return the previously cached value. This can be useful in LINQ expressions especially.
There is a new monadic type called TryAsync
which, as you may have guessed, is an asynchronous version of Try
. Try
and TryAsync
are delegate types that are invoked when you call extension methods like Match
on them. By default the TryAsync
evaluation extension methods will wrap the invocation in a Task
and will catch any exceptions thrown.
TryAsync<int> LongRunningOp() => TryAsync(() => 10);
int x = await LongRunningOp().Match(
Succ: y => y * 2,
Fail: ex => 0
);
Unfortunately you must wrap the operation in a TryAsync(() => ...)
because the compiler can't infer the result-type like it can with Try
. However you can promote a Try
to a TryAsync
:
Try<int> LongRunningOp() => () => 10;
int x = await LongRunningOp().ToAsync().Match(
Succ: y => y * 2,
Fail: ex => 0
);
Or use any of the new Async
extension methods added to Try
:
Try<int> LongRunningOp() => () => 10;
int x = await LongRunningOp().MatchAsync(
Succ: y => y * 2,
Fail: ex => 0
);
Every single method of Try
now has an Async
variant. Also any method of Try
or TryAsync
that takes a Func
(for example Map(Try<A> x, Func<A, B> f)
), now has a variant that allows a Task
to be returned instead. For Try
methods this will either promote the result to be a TryAsync
(as is the case with Map
), or a Task
(as is the case with Fold
). This makes it trivial to deal with asynchronous results, as the Try
or TryAsync
will automatically perform the correct synchronisation required to extract a valid result.
For functions where operations could run in parallel then the type will again handle that automatically. For example if you use the new Applicative
functionality, this is possible:
// Placeholder functions. Imagine they're doing some work to get some remote
// data lists.
TryAsync<Lst<int>> GetListOneFromRemote() => TryAsync(List(1, 2, 3));
TryAsync<Lst<int>> GetListTwoFromRemote() => TryAsync(List(4, 5, 6));
// Combines two lists and sorts them.
public static IEnumerable<int> CombineAndOrder(Lst<int> x, Lst<int> y) =>
from item in (x + y)
orderby item
select item;
// Uses the fact that TryAsync is an applicative, and therefore has the
// apply function available. The apply function will run all three parts
// of the applicative asynchronously, will handle errors from any term,
// and will then apply them to the CombineAndOrder function.
public TryAsync<IEnumerable<int>> GetRemoteListsAndCombine() =>
apply(
CombineAndOrder,
GetListOneFromRemote(),
GetListTwoFromRemote());
[Fact]
public async void ListCombineTest()
{
var res = await GetRemoteListsAndCombine().IfFail(Enumerable.Empty<int>());
var arr = res.ToArray();
Assert.True(arr[0] == 1);
Assert.True(arr[1] == 2);
Assert.True(arr[2] == 3);
Assert.True(arr[3] == 4);
Assert.True(arr[4] == 5);
Assert.True(arr[5] == 6);
}
As Try
has got its TryAsync
pairing, so has TryOption
now got TryOptionAsync
. The interface is almost exactly the same as TryAsync
. Here are some quick examples:
// Some example method prototypes
public TryOptionAsync<int> LongRunningAsyncTaskOp() => TryOptionAsync(10);
public Task<TryOption<int>> LongRunningAsyncOp() => Task.FromResult(TryOption(10));
public TryOption<int> LongRunningOp() => TryOption(10);
public Task<int> MapToTask(int x) => Task.FromResult(x * 2);
public int MapTo(int x) => x * 2;
public async void Test()
{
TryOptionAsync<int> j = LongRunningOp().ToAsync();
TryOptionAsync<int> k = LongRunningAsyncOp().ToAsync();
// These run synchronously
int a = LongRunningOp().IfNoneOrFail(0);
TryOption<int> b = LongRunningOp().Map(MapTo);
// These run asynchronously
int u = await LongRunningAsyncTaskOp().IfNoneOrFail(0);
int v = await LongRunningAsyncOp().IfNoneOrFail(0);
int x = await LongRunningOp().IfNoneOrFailAsync(0);
TryOptionAsync<int> y1 = LongRunningOp().MapAsync(MapTo);
TryOptionAsync<int> y2 = LongRunningOp().MapAsync(MapToTask);
int z1 = await y1.IfNoneOrFail(0);
int z2 = await y2.IfNoneOrFail(0);
}
Ad-hoc polymorphism has long been believed to not be possible in C#. However with some cunning it is. Ad-hoc polymorphism allows programmers to add traits to a type later. For example in C# it would be amazing if we had an interface called INumeric
for numeric types like int
, long
, double
, etc. The reason this doesn't exist is if you write a function like:
INumeric Add(INumeric x, INumeric y) => x + y;
Then it would cause boxing. Which is slow (well, slower). I can only assume that's why it wasn't added by the BCL team. Anyway, it's possible to create a numeric type, very much like a type-class in Haskell, and ad-hoc instances of the numeric type-class that allow for generic numeric operations without boxing.
From now on I will call them type-classes and class-instances, or just instances. This is not exactly the same as Haskell's type-classes. If anything it's closer to Scala's implicits. However to make it easier to discuss them I will steal from Haskell's lexicon.
So for example, this is how to create a number type-class:
public interface Num<A>
{
A Add(A x, A b);
A Subtract(A x, A b);
...
}
Notice how there are two arguments to Add
and Subtract
. Normally if I was going to implement this interface
the left-hand-side of the Add
and Subtract
would be this
. I will implement the ad-hoc class-instance to demonstrate why that is:
public struct TInt : Num<int>
{
public int Add(int x, int b) => x + y;
public int Subtract(int x, int b) => x + y;
...
}
See how TInt
is a struct
? Structs have a useful property in C# in that they can't be null
. So we can invoke the operations like so:
int r = default(TInt).Add(10, 20);
The important thing to note is that default(TInt)
gets optimisied out in a release build, so there's no cost to the invocation of Add
. The Add
and Subtract
methods both take int
and return int
. So therefore there's no boxing at all.
If we now implement TFloat
:
public struct TFloat : Num<float>
{
public float Add(float x, float b) => x + y;
public float Subtract(float x, float b) => x + y;
...
}
Then we can see how a general function could be written to take any numeric type:
public A DoubleIt<NumA, A>(A x) where NumA : struct, Num<A> =>
default(NumA).Add(x, x);
The important bit is the NumA
generic argument, and the constraint of struct, Num<A>
. That allows us to call default(NumA)
to get the type-class instance and invoke Add
.
And so this can now be called by:
int a = DoubleIt<TInt, int>(5); // 10
double b = DoubleIt<TFloat, float>(5.25); // 10.5
By expanding the amount of operations that the Num<A>
type-class can do, you can perform any numeric operation you like. If you like you can add new numeric types (say for complex numbers, or whatever), where the rules of the type are kept in the ad-hoc instance.
Luckily you don't need to do that, because I have created the Num<A>
type (in the LanguageExt.TypeClasses
namespace), as well as Floating<A>
(with all of the operations from Math
; like Sin
, Cos
, Exp
, etc.). Num<A>
also has a base-type of Arithmetic<A>
which supports Plus
, Subtract
, Product
, Negate
. This is for types which don't need the full spec of the Num<A>
type. I have also mapped all of the core numeric types to instances: TInt
, TShort
, TLong
, TFloat
, TDouble
, TDecimal
, TBigInt
, etc. So it's possible to write truly generic numeric code once.
There's no getting around the fact that providing the class-instance in the generic arguments list is annoying (and later you'll see how annoying). The Roslyn team are looking into a type-classes like feature for a future version of C# (variously named: 'Concepts' or 'Shapes'). So this will I'm sure be rectified, and when it is, it will be implemented exactly as I am using them here.
Until then the pain of providing the generic arguments must continue. You do however get a super-powered C# in the mean-time.
The need to write this kind of super-generic code is rare; but when you need it, you need it - and right now this is simply the most powerful way.
Next up is Eq<A>
. Equality testing in C# is an absolute nightmare. From the different semantics of Eqauls
and ==
, to IEqualityComparer
, and the enormous hack which is EqualityComparer.Default
(which doesn't blow up at compile-time if your code is wrong).
The Eq<A>
type-class looks like this:
public interface Eq<A>
{
bool Equals(A x, A y);
int GetHashCode(A x);
}
There are Eq
prefixed instances for all common types (EqInt
, EqString
, EqGuid
etc.), as well as for all of the types in this library (EqLst
, EqSet
, EqTry
, etc). All of the numeric types (TInt
, TDouble
, etc.) also implement Eq<A>
.
To make it slightly prettier to use in code, you can use the Prelude
equals
function:
bool x = equals<EqInt>(1, 1); // True
Or use default
as shown before:
bool x = default(EqInt).Equals(1, 1); // True
One final way is:
bool x = EqInt.Inst.Equals(1, 1);
Inst
is defined on all of the instances in lang-ext, but it's not an 'official feature'. Anybody could implement an ad-hoc implementation of Eq<A>
and not provide an Inst
.
For example you may call this directly:
bool x = EqLst.Inst.Equals(List(1,2,3), List(1,2,3)); // true
Because you may be concerned about calling:
bool x = List(1,2,3) == List(1,2,3); // ?
... as all C# programmers are at some point, because we have no idea most of the time whether ==
does what we think it should.
Just FYI
List(1,2,3) == List(1,2,3)
does work properly! As do all types in language-ext.
There are two variants of the immutable HashSet
in language-ext:
HashSet<A>
HashSet<EqA, A> where EqA : struct, Eq<A>
What's interesting about the second one is that the equality definition is baked into the type. So this:
HashSet<EqString, string>
Is not compatible with:
HashSet<EqStringOrdinalIgnoreCase, string>
And if you think about that, it's right. The strings that are used as keys in the HashSet<EqString, string>
do not have the same properties as the strings in HashSet<EqStringOrdinalIgnoreCase, string>
. So even though they're both strings, they have different semantics (which cause wildly different behaviour for things like set intersection, unions, etc.)
Now compare that to HashSet<T>
in the BCL, or ImmutableHashSet<T>
in System.Collections.Immutable
, where two different sets with different IEqualityComparer
types injected will cause undefined results when used together.
That's hopefully a small glimpse into the potential for improving type-safeness in C#.
Ord
is for ordering. i.e. a IComparable
replacement. By the way, these names Eq
, Ord
, Num
, are all lifted from Haskell. I much prefer the short concise names that still convey meaning than the bulky and often clumsy names of the BCL.
This is Ord<A>
, it derives from Eq<A>
public interface Ord<A> : Eq<A>
{
int Compare(A x, A y);
}
Usage should be self-explanatory now, but the important thing to note here is that because 'type classes' are just interfaces, they can also have an inheritance hierarchy.
This is a slightly more complex example:
public struct OrdArray<ORD, A> : Ord<A[]>
where ORD : struct, Ord<A>
{
public int Compare(A[] mx, A[] my)
{
if (ReferenceEquals(mx, my)) return 0;
if (ReferenceEquals(mx, null)) return -1;
if (ReferenceEquals(my, null)) return 1;
var cmp = mx.Length.CompareTo(my.Length);
if (cmp == 0)
{
for(var i = 0; i < mx.Length; i++)
{
cmp = default(ORD).Compare(mx[i], my[i]);
if (cmp != 0) return cmp;
}
return 0;
}
else
{
return cmp;
}
}
public bool Equals(A[] x, A[] y) =>
default(EqArray<ORD, A>).Equals(x, y);
public int GetHashCode(A[] x) =>
hash(x);
}
The OrdArray
which is an Ord<A[]>
, does itself also take an ORD
generic argument, which allows the contents of the array to be compared:
int x = OrdArray<TInt, int>.Inst.Compare(Array(1,2), Array(1,2)); // 0
This is where we start going a little more abstract. Semigroups are a feature of category theory, which is soooo not important for this discussion. They represent an associative binary operation, which can be invoked by calling Append
.
public interface Semigroup<A>
{
A Append(A x, A y);
}
Positive numbers (for example) form a semigroup. I won't dwell on it too long, because although the Append
function is super-useful, nearly everything that falls into the Semigroup
category is also a Monoid
...
A monoid has something that a semigroup doesn't, and that's the concept of identity (often meaning 'empty' or 'zero'). It looks like this:
public interface Monoid<A> : Semigroup<A>
{
A Empty();
}
This comes with some helper functions in LanguageExt.TypeClass
:
public static partial class TypeClass
{
public static A mempty<MONOID, A>() where MONOID : struct, Monoid<A> =>
default(MONOID).Empty();
public static A mconcat<MONOID, A>(IEnumerable<A> xs) where MONOID : struct, Monoid<A> =>
xs.Fold(mempty<MONOID, A>(), (s, x) => append<MONOID, A>(s, x));
public static A mconcat<MONOID, A>(params A[] xs) where MONOID : struct, Monoid<A> =>
xs.Fold(mempty<MONOID, A>(), (s, x) => append<MONOID, A>(s, x));
}
Now the semigroup Append
comes to life. Examples of monoids are: TInt
, MLst
, TString
, etc. i.e.
var x = mconcat<TString, string>("Hello", " ", "World"); // "Hello World"
var y = mconcat<TLst<int>, Lst<int>>(List(1), List(2, 3)); // [1,2,3]
var z = mconcat<TInt, int>(1, 2, 3, 4, 5); // 15
The Empty()
function is what provides the identity value for the concat operations. So for string
it's ""
, for Lst<T>
it's []
and for int
it's 0
. So a monoid is a semigroup with a zero.
It's surprising how much stuff just starts working when you know your type is a monoid. For example in language-ext version 1 there is a monadic type called Writer<W, T>
. The writer monad collects a log as well as returning the bound value. In version 1 the log had to be an IEnumerable<W>
, which isn't super flexible. In language-ext version 2 the type looks like this:
public class Writer<MonoidW, W, A> where MonoidW : struct, Monoid<W>
{
...
}
So now it can be a running numeric total, or a Lst<W>
, or a Set<W>
, or whatever monoid you dream up.
Higher-order polymorphism would allow us to define a type like so:
public interface MyType<M<A>>
{
M<B> Foo<B>(M<A> ma);
}
Where not only is the A
parametric, but so it M
. So for example if I wanted to implement MyType
for Option<A>
I could do:
public class MyOptionType<A> : MyType<Option<A>>
{
public Option<B> Foo<B>(Option<A> ma) => ...;
}
It would be soooo nice if C# (well, the immutable CLR) would support this. But it doesn't. So we need to find ways around it. The way I am using for language-ext is:
public interface MyType<MA, A>
{
MB Foo<MB, B>(MA ma);
}
public class MyOptionType<A> : MyType<Option<A>, A>
{
public MB Foo<MB, B>(Option<A> ma) => ...;
}
This is where some of the difficulties come in. How do we return an MB
if we don't know what it is? This is a problem for the Monad
type. This is a simplified version:
public interface Monad<MA, A>
{
MB Bind<MB, B>(MA ma, Func<B, MB> bind);
MA Return(A a);
MA Fail(Exception e = null);
}
Looking at the prototype for Bind
it seems at first glance that the bind
argument will give us the MB
value we want. But an Option
might be in a None
state, in which case it shouldn't run bind
.
public MB Bind<MB, B>(Option<A> ma, Func<A, MB> bind) =>
ma.IsSome
? bind(ma.Value)
: ??? ; // What do we return here?
The key is to use constraints. But it also requires an extra generic paramter for Bind
:
public interface Monad<MA, A>
{
MB Bind<MonadB, MB, B>(MA ma, Func<B, MB> bind)
where MonadB : struct, Monad<MB, B>;
MA Return(A a);
MA Fail(Exception e = null);
}
So we now know that MonadB
is a class-instance of the Monad<MB, B>
type-class. So we can now do this:
public MB Bind<MB, B>(Option<A> ma, Func<A, MB> f)
where MonadB : struct, Monad<MB, B> =>
ma.IsSome
? f(ma.Value)
: default(MonadB).Fail();
The eagle eyed reader will notice that this actually allows binding to any resulting monad (not just Option<B>
). I'm not an academic, but I'm sure there will be someone throwing their toys out of their prams at this point. However, it works, it's type-safe, and it's efficient.
The actual definition of Monad
is more complex than this, in order to unify monadic types that take arguments (Reader
and State
) and monads that carry internal state (Writer
and State
), as well as to support asynchronous monads (TryAsync
and TryOption
). I won't muddy the waters too much right now, but unified and type-safe they are. There are no hacks.
You should see that the Return
and Fail
functions are trivial to implement:
public Option<A> Return(A a) =>
Optional(a);
public Option<A> Fail(Exception e = null) =>
None;
What that means is that any function that has been constrained by a monad instance can create new instances of them:
public M CreateNewIntegerMonad<MonadInt, M, int>(int x)
where MonadInt : struct, Monad<M, int> =>
default(MonadInt).Return(x);
This is one of the key breakthroughs. Imagine trying to create a Monad
type the old way:
public interface Monad<A>
{
Monad<B> Bind<B>(Func<A, Monad<B>> bind);
}
public class Option<A> : Monad<A>
{
public Monad<B> Bind<B>(Monad<A> ma, Func<A, Monad<B>> bind) =>
IsSome
? bind(Value)
: None;
}
public Monad<int> CreateNewIntegerMonad(int x) =>
????; // How?
Maybe we could parameterise it?
public Monad<int> CreateNewIntegerMonad<M>(int x) where M : Monad<int> =>
????; // We still can't call new M(x)
But that doesn't work either because we still can't call new M(x)
. Being able to paramterise generic functions at the point where you know the concrete types (and therefore know the concrete class-instance) means that the generic functions can invoke the instance functions to create the concrete types.
Here's a super generic example of a function that takes two monad arguments, they're both of the same type, and their bound values are Num<A>
.
public static MA Add<MonadA, MA, NumA, A>(MA ma, MA mb)
where MonadA : struct, Monad<MA, A>
where NumA : struct, Num<A> =>
default(MonadA).Bind<MonadA, MA, A>(ma, a =>
default(MonadA).Bind<MonadA, MA, A>(mb, b =>
default(MonadA).Return(default(NumA).Plus(a, b))));
You may notice that the two Bind
calls followed by the Return
are basically a much less attractive version of this:
from a in ma
from b in mb
select default(NumA).Plus(a, b);
And so I can now add two options:
var x = Some(10);
var y = Some(20);
var z = Option<int>.None;
var r1 = Add<MOption<int>, Option<int>, TInt, int>(x, y); // Some(30)
var r2 = Add<MOption<int>, Option<int>, TInt, int>(x, z); // None
Assert.True(r1 == Some(30));
Assert.True(r2 == None);
Or two lists:
var x = List(1, 2, 3);
var y = List(4, 5, 6);
var z = List<int>();
var r1 = Add<MLst<int>, Lst<int>, TInt, int>(x, y);
var r2 = Add<MLst<int>, Lst<int>, TInt, int>(x, z);
Assert.True(r1 == List(5, 6, 7, 6, 7, 8, 7, 8, 9));
Assert.True(r2 == z);
Or any two monads. They will follow the built in rules for the type, and produce concrete values efficiently and without any boxing or dynamic casting.
Because using the super-generic stuff is hard, and most of the time not needed. I have kept the transformer types, but they're now implemented in terms of the instance types. There is a new MonadTrans
type-class and a default instance called Trans
.
For every pair of nested monads: Lst<Option<A>>
, Try<Either<L, A>>
, etc. there are the following extension methods (this is for Arr<Lst<A>>
):
A SumT<NumA, A>(this Arr<Lst<A>> ma);
int CountT<A>(this Arr<Lst<A>> ma);
Arr<Lst<B>> BindT<A, B>(this Arr<Lst<A>> ma, Func<A, Lst<B>> f);
Lst<Arr<B>> Traverse<A, B>(this Arr<Lst<A>> ma, Func<A, B> f);
Lst<Arr<A>> Sequence<A>(this Arr<Lst<A>> ma);
Arr<Lst<B>> MapT<A, B>(this Arr<Lst<A>> ma, Func<A, B> f);
S FoldT<S, A>(this Arr<Lst<A>> ma, S state, Func<S, A, S> f);
S FoldBackT<S, A>(this Arr<Lst<A>> ma, S state, Func<S, A, S> f);
bool ExistsT<A>(this Arr<Lst<A>> ma, Func<A, bool> f);
bool ForAllT<A>(this Arr<Lst<A>> ma, Func<A, bool> f);
Unit IterT<A>(this Arr<Lst<A>> ma, Action<A> f);
Arr<Lst<A>> FilterT< A>(this Arr<Lst<A>> ma, Func<A, bool> pred);
Arr<Lst<A>> Where<A>(this Arr<Lst<A>> ma, Func<A, bool> pred);
Arr<Lst<A>> Select<A, B>(this Arr<Lst<A>> ma, Func<A, B> f);
Arr<Lst<C>> SelectMany<A, B, C>(
this Arr<Lst<A>> ma,
Func<A, Lst<B>> bind,
Func<A, B, C> project);
Arr<Lst<A>> PlusT<NUM, A>(this Arr<Lst<A>> x, Arr<Lst<A>> y) where NUM : struct, Num<A>;
Arr<Lst<A>> SubtractT<NUM, A>(this Arr<Lst<A>> x, Arr<Lst<A>> y) where NUM : struct, Num<A>;
Arr<Lst<A>> ProductT<NUM, A>(this Arr<Lst<A>> x, Arr<Lst<A>> y) where NUM : struct, Num<A> =>
ApplyT(default(NUM).Product, x, y);
Arr<Lst<A>> DivideT<NUM, A>(this Arr<Lst<A>> x, Arr<Lst<A>> y) where NUM : struct, Num<A>;
AppendT<SEMI, A>(this Arr<Lst<A>> x, Arr<Lst<A>> y) where SEMI : struct, Semigroup<A>;
int CompareT<ORD, A>(this Arr<Lst<A>> x, Arr<Lst<A>> y) where ORD : struct, Ord<A>;
bool EqualsT<EQ, A>(this Arr<Lst<A>> x, Arr<Lst<A>> y) where EQ : struct, Eq<A>;
Arr<Lst<A>> ApplyT<A, B>(this Func<A, B> fab, Arr<Lst<A>> fa);
Arr<Lst<C>> ApplyT<A, B, C>(this Func<A, B, C> fabc, Arr<Lst<A>> fa, Arr<Lst<A>> fb);
The number of functions has increased dramatically. Some of the special ones are Traverse
and Sequence
which flips the inner and outer types. So for example:
Lst<Option<int>> x = List(Some(1), Some(2), Some(3), Some(4), Some(5));
Option<Lst<int>> y = x.Sequence();
Assert.True(y == Some(List(1, 2, 3, 4, 5)));
As you can see, the list is now inside the option.
Lst<Option<int>> x = List(Some(1), Some(2), Some(3), None, Some(5));
Option<Lst<int>> y = x.Sequence();
Assert.True(y == None);
In this case there is a None
in the Lst
so when the Lst<Option<>>
becomes a Option<Lst<>>
the rules of the Option
take over, and one None
means all None
.
This can be quite useful for Either
:
var x = List<Either<string, int>>(1, 2, 3, 4, "error");
var y = x.Sequence();
Assert.True(y.IsLeft && y == "error");
This collects the first error it finds, or returns Right
if there is no error.
Traverse
is the same as Sequence
except it applies a mapping function to each bound value as it's transforming the types. Here's an example that runs 6 tasks in parallel, and collects their results:
var start = DateTime.UtcNow;
var f1 = Task.Run(() => { Thread.Sleep(3000); return 1; });
var f2 = Task.Run(() => { Thread.Sleep(3000); return 2; });
var f3 = Task.Run(() => { Thread.Sleep(3000); return 3; });
var f4 = Task.Run(() => { Thread.Sleep(3000); return 4; });
var f5 = Task.Run(() => { Thread.Sleep(3000); return 5; });
var f6 = Task.Run(() => { Thread.Sleep(3000); return 6; });
var res = await List(f1, f2, f3, f4, f5, f6).Traverse(x => x * 2);
Assert.True(toSet(res) == Set(2, 4, 6, 8, 10, 12));
var ms = (int)(DateTime.UtcNow - start).TotalMilliseconds;
Assert.True(ms < 3500, $"Took {ms} ticks");
So there is a List of Tasks that becomes a single awaitable Task of List.
As well as the extensions, there are also static classes for the transformer types. There is one for each type of monad. So for example, Option
has a LanguageExt.OptionT
type. Whenever you have a pair of nested monads, and Option
is the inner monad, then you would use OptionT
:
var ma = List(Some(1),Some(2),Some(3),Some(4),Some(5));
var total = OptionT.foldT(ma, 0, (s, x) => s + x); // 15
var total = OptionT.sumT<TInt, int>(ma); // 15
var mb = OptionT.filterT(ma, x > 3); // List(Some(3), Some(4))
I could go on endlessly about the new types. There are so many. But for the release notes I think I should wrap it up. It's worth taking a look at the API documentation for the type-classes and the instances
Although this doesn't really fit with the core message of the library, it is something used internally in the project, and because it's super useful, I've decided to make it public
rather than private
. There are 4 functions for building a Func<...,R>
which invokes the constructor for a type.
var ticks = new DateTime(2017, 1, 1).Ticks;
// Builds a delegate to call new DateTime(long);
var ctor = IL.Ctor<long, DateTime>();
DateTime res = ctor(ticks);
Assert.True(res.Ticks == ticks);
The generated delegate doesn't use reflection, IL is emitted directly. So invocation is as fast as calling new DateTime(ticks)
. The four Ctor
functions are for up to four arguments.
The NewType
system uses it to build a fast strongly-typed factory function:
public abstract class NewType<NEWTYPE, A, PRED> :
IEquatable<NEWTYPE>,
IComparable<NEWTYPE>
where PRED : struct, Pred<A>
where NEWTYPE : NewType<NEWTYPE, A, PRED>
{
protected readonly A Value;
/// <summary>
/// Constructor function
/// </summary>
public static readonly Func<A, NEWTYPE> New = IL.Ctor<A, NEWTYPE>();
...
}
As you can see, it would be impossible to call new NEWTYPE(x)
from the NewType
base-class.
So for example:
class Metres : NewType<Metres, float>
{
public Metres(float x) : base(x) {}
}
var ms = Metres.New(100);