Skip to content

Releases: louthy/language-ext

Lenses in C#

11 Apr 22:15
a487923
Compare
Choose a tag to compare

Lenses allow for bi-directional transformation of immutable value types. This is particularly needed when immutable types are composed and transformation results in many nested With functions or getters and setters.

Transformation of immutable types

If you're writing functional code you should treat your types as values. Which means they should be immutable. One common way to do this is to use readonly fields and provide a With function for mutation. i.e.

public class A
{
    public readonly X X;
    public readonly Y Y;

    public A(X x, Y y)
    {
        X = x;
        Y = y;
    }

    public A With(X X = null, Y Y = null) =>
        new A(
            X ?? this.X,
            Y ?? this.Y
        );
}

Then transformation can be achieved by using the named arguments feature of C# thus:

val = val.With(X: x);

val = val.With(Y: y);

val = val.With(X: x, Y: y);

[With]

It can be quite tedious to write the With function however. And so, if you include the LanguageExt.CodeGen nu-get package in your solution you gain the ability to use the [With] attribtue on a type. This will build the With method for you.

NOTE: The LanguageExt.CodeGen package and its dependencies will not be included in your final build - it is purely there to generate the code.

You must however:

  • Make the class partial
  • Have a constructor that takes the fields in the order they are in the type
  • The names of the arguments should be the same as the field, but with the first character lower-case

i.e.

[With]
public partial class A
{
    public readonly X X;
    public readonly Y Y;

    public A(X x, Y y)
    {
        X = x;
        Y = y;
    }
}

Transformation of nested immutable types with Lenses

One of the problems with immutable types is trying to transform something nested deep in several data structures. This often requires a lot of nested With methods, which are not very pretty or easy to use.

Enter the Lens<A, B> type.

Lenses encapsulate the getter and setter of a field in an immutable data structure and are composable:

[With]
public partial class Person
{
    public readonly string Name;
    public readonly string Surname;

    public Person(string name, string surname)
    {
        Name = name;
        Surname = surname;
    }

    public static Lens<Person, string> name =>
        Lens<Person, string>.New(
            Get: p => p.Name,
            Set: x => p => p.With(Name: x));

    public static Lens<Person, string> surname =>
        Lens<Person, string>.New(
            Get: p => p.Surname,
            Set: x => p => p.With(Surname: x));
}

This allows direct transformation of the value:

var person = new Person("Joe", "Bloggs");

var name = Person.name.Get(person);
var person2 = Person.name.Set(name + "l", person);  // Joel Bloggs

This can also be achieved using the Update function:

var person = new Person("Joe", "Bloggs");

var person2 = Person.name.Update(name => name + "l", person);  // Joel Bloggs

The power of lenses really becomes apparent when using nested immutable types, because lenses can be composed. So, let's first create a Role type which will be used with the Person type to represent an employee's job title and salary:

[With]
public partial class Role
{
    public readonly string Title;
    public readonly int Salary;

    public Role(string title, int salary)
    {
        Title = title;
        Salary = salary;
    }

    public static Lens<Role, string> title =>
        Lens<Role, string>.New(
            Get: p => p.Title,
            Set: x => p => p.With(Title: x));

    public static Lens<Role, int> salary =>
        Lens<Role, int>.New(
            Get: p => p.Salary,
            Set: x => p => p.With(Salary: x));
}

[With]
public partial class Person
{
    public readonly string Name;
    public readonly string Surname;
    public readonly Role Role;

    public Person(string name, string surname, Role role)
    {
        Name = name;
        Surname = surname;
        Role = role;
    }

    public static Lens<Person, string> name =>
        Lens<Person, string>.New(
            Get: p => p.Name,
            Set: x => p => p.With(Name: x));

    public static Lens<Person, string> surname =>
        Lens<Person, string>.New(
            Get: p => p.Surname,
            Set: x => p => p.With(Surname: x));

    public static Lens<Person, Role> role =>
        Lens<Person, Role>.New(
            Get: p => p.Role,
            Set: x => p => p.With(Role: x));
}

We can now compose the lenses within the types to access the nested fields:

var cto = new Person("Joe", "Bloggs", new Role("CTO", 150000));

var personSalary = lens(Person.role, Role.salary);

var cto2 = personSalary.Set(170000, cto);

[WithLens]

Typing the lens fields out every time is even more tedious than writing the With function, and so there is code generation for that too: using the [WithLens] attribute. Next, we'll use some of the built-in lenses in the Map type to access and mutate a Appt type within a map:

[WithLens]
public partial class Person : Record<Person>
{
    public readonly string Name;
    public readonly string Surname;
    public readonly Map<int, Appt> Appts;

    public Person(string name, string surname, Map<int, Appt> appts)
    {
        Name = name;
        Surname = surname;
        Appts = appts;
    }
}

[WithLens]
public partial class Appt : Record<Appt>
{
    public readonly int Id;
    public readonly DateTime StartDate;
    public readonly ApptState State;

    public Appt(int id, DateTime startDate, ApptState state)
    {
        Id = id;
        StartDate = startDate;
        State = state;
    }
}

public enum ApptState
{
    NotArrived,
    Arrived,
    DNA,
    Cancelled
}

So, here we have a Person with a map of Appt types. And we want to update an appointment state to be Arrived:

// Generate a Person with three Appts in a Map
var person = new Person("Paul", "Louth", Map(
    (1, new Appt(1, DateTime.Parse("1/1/2010"), ApptState.NotArrived)),
    (2, new Appt(2, DateTime.Parse("2/1/2010"), ApptState.NotArrived)),
    (3, new Appt(3, DateTime.Parse("3/1/2010"), ApptState.NotArrived))));

// Local function for composing a new lens from 3 other lenses
Lens<Person, ApptState> setState(int id) => 
    lens(Person.appts, Map<int, Appt>.item(id), Appt.state);

// Transform
var person2 = setState(2).Set(ApptState.Arrived, person);

Notice the local-function which takes an ID and uses that with the item lens in the Map type to mutate an Appt. Very powerful stuff.

There are a number of useful lenses in the collection types that can do common things like mutate by index, head, tail, last, etc.

Everything else

  • Added ma.Flatten() and flatten(ma) monadic join for all monadic types
  • Added Indexable type-class and class-instances for all indexable types
  • Performance improvements for enumerable bind and enumerable concat (which should also protect against the Stack Overflow on .NET 4.6)
  • Fix for BindT being eager with IEnumerable
  • Deprecated some duplicate extensions for Task
  • [Pure] attribute should now propagate to the assemblies

All on nu-get now.

Record fix release + many other minor fixes

08 Feb 11:43
49bc1dd
Compare
Choose a tag to compare

Seq bug fix / Do operation additions

10 Nov 10:38
e5a683c
Compare
Choose a tag to compare

There was an intermittent problem with lazy Seq which would occasionally cause iteration to skip an item in the sequence. This could happen if two iterators were consuming the sequence separately (not a threading issue, but a state issue). Thanks to Tom @colethecoder for fixing that issue.

Anybody using Language-Ext 3.1.* will want to upgrade to avoid this issue.

There is a new Do function on every monadic and collection type. It's the same as Iter but returns the original container type. This allows for fluent side-effecting code.

On nu-get now.

Seq performance refactor

17 Oct 11:00
Compare
Choose a tag to compare
Pre-release

Seq<A> refactor

I have refactored the Seq<A> to have similar performance characteristics to the .NET List<T> type (under most circumstances).

It works in almost the same way as List<T> internally: which is that it is an array which doubles in size when it needs more space. This has a number of benefits for Seq<A>:

  • The items in the sequence are contiguous in memory. This has massive performance benefits when iterating the sequence, because it gives the cache on the CPU a chance to page in as much of the sequence as it can.
  • There is now an Add and Concat which has very similar performance characteristics as Cons. And so the sequence can grow in either direction without incuring a performance cost. This is particularly useful if you're using the mconcat or mappend with the MSeq monoid.
  • When not doubling in size the Cons and Add operations are almost as fast as setting an item in an array.
  • Seq<A> now gains an index accessor for O(1) access to items in the non-lazy sequence. A lazy sequence is O(n) where n are the number of items that need to be streamed to get to the index. Obviously after that the sequence becomes non-lazy.

Other Seq<A> features

Seq<A> now has a deconstructor that can extract the head item and the tail:

    var (head, tail) = seq;

Note, this will throw for empty sequences.

Lazy sequences can be forced to load all of their items with:

seq = seq.Strict();

What are the problems?

  • Complexity

The Seq type is internally a lot more complex now, even if the surface API is the same. That's why I'm releasing this as a beta. @StefanBertels @michael-wolfenden @bender2k14 @faeriedust @colethecoder @StanJav @OlduwanSteve - you guys tend to be very good at spotting issues, so an extra set of eyes on this would be good.

Unit tests-wise I think we're pretty good, as my initial tests showed that half of the unit tests failed from the initial bugs (because Seq is used so much). Those same tests going reassuringly green gave a reasonable amount of confidence in the approach. But if anyone wants to add more, that would be always appreciated.

Tradeoffs

As with any immutable data structure, there are some trade-offs when it comes to creating mutated versions.

With the new Seq<A> the tradeoff comes when calling multiple Cons or Add operations from the same Seq<A>. Because internally the structure is an array, it is only one dimensional. It can only grow left (Cons) or grow right (Add). The vast majority of add and cons operations will be working in that single dimension, and so doing an add or cons will have similar performance characteristics to setting an element in an array.

But when multiple cons or adds are done from the same Seq<A> you add additional dimensions to the sequence.

    A -> A -> A -> A ->  A -> 
                     \
                      -> A ->  A -> ...

There are then multiple dimensions that want to expand into the same single dimensional array. So, in these circumstances the routing node gets a flag set after the first Cons, which any subsequent Cons will see and, instead of just expanding into the array, will clone the whole Seq.
If this happens relatively infrequently, or your sequence is small, then it's not a big deal. But if you have a million item list, that you keep just consing a single item onto then you're going to be doing a lot of array copying.

So, just to be clear, this is fine:

   var xs = Seq(Range(1, 10000000));
   var nxs = 1.Cons(2.Cons(3.Cons(xs)));

This has the biggest cost:

   var xs = Seq(Range(1, 10000000));

   var nxs1 = 1.Cons(xs);  // This is free
   var nxs2 = 2.Cons(xs);  // This is expensive
   var nxs3 = 3.Cons(xs);  // This is expensive
   var nwb = 4.Cons(nxs3); // This is free

Thankfully I don't think this is a very likely or common use-case for Seq and the most common use-cases are now going to be significantly more efficient. Note: Once a Seq has branched into multiple dimensions the individual dimensions are free to grow as though they were singly dimensional, and so it's only the branch which causes copying.

Record type attributes

The attributes for opting out of various Record features have been bothering me. They're pretty ugly naming-wise, so I have deprecated the old ones and added a few more:

Old attribute New attribute
OptOutOfEq [NonEq]
OptOutOfOrd [NonOrd]
OptOutOfToString [NonShow]
OptOutOfHashCode [NonHash]
OptOutOfSerialization [NonSerializable]

One thing that was bugging me was that if I wanted a field or property to be completely ignored by the Record system then I'd have to use:

[OptOutOfEq, OptOutOfHashCode, OptOutOfOrd, OptOutOfSerialization, OptOutOfToString]

Which is crazy. And so now you can just use:

[NonRecord]

It makes the field or property invisible to the Record system.

You can also use:

[NonStructural]

Which is the equivalent of [NonEq, NonOrd, NonHash] to remove the field or property from structural equality and ordering.

Conclusion

I welcome feedback on these changes. This is a beta release because it's feature complete, but risky. And so I will be taking soundings for a few weeks.

Minor breaking change to `memo`

24 Sep 13:16
Compare
Choose a tag to compare

Minor breaking change to:

Func<A> memo<A>(Func<A> f);

This previously used Lazy<T> internally to lazily load the value and support at most once evaluation. But because Lazy<T> memoises exceptions, which I believe is undesirable, I have decided to change this to a bespoke implementation that doesn't memoise exceptions.

Possible breaking change

04 Jul 16:40
897571a
Compare
Choose a tag to compare

The Task<A> extension methods have been moved from the global namespace into the LanguageExt namespace, so please make sure you're using LanguageExt with the latest release.

Some additional helper functions in Try, TryOption, TryAsync, TryOptionAsyncto help convert toEitherandValidation. You can now provide your own mapping function to map between Exception` and whatever error type you want. i.e.

    var ma = tryValue.ToEither(ex => Error.New(ex.Message));

It's still usually best to provide bespoke extensions to do this mapping, but this should make the job a bit simpler.

There is also an additional BindLeft function on Either, EitherUnsafe, and EitherAsync types.

Rx features split out to LanguageExt.Rx

04 Jun 18:32
f76c8e9
Compare
Choose a tag to compare

I have decided to relent and break the Rx related functionality out of the LanguageExt.Core project. I did so for a few reasons:

  • I kept being asked. And after a time one realises that there must be an issue.
  • I decided that the Observable Prelude functionality wasn't important enough to keep in the Prelude type (which was one of the key reasons to keep it together before)
  • There has been some odd versioning going on with Rx where I've seen issues in other projects that have required manual assembly version matching
  • Having zero dependencies for the Core is nice.

The new LanguageExt.Rx project only supports net46+ and netstandard 2, this is because I set the project to have a dependency on System.Reactive version *, which in theory should allow any version to be included, but it seems to have locked it to 4.* (slightly annoying, but it's better than locking it to an older version I suppose) - if anyone has information on how to make the wildcard work for all versions please get in touch via the github Issues.

Version 3 of Language-Ext released

14 Apr 22:32
Compare
Choose a tag to compare

Today I have decided to release version 3 of lang-ext, this has been running along as a beta for many months now and has been stable for all of that period and so although there was much more stuff I wanted to put into the release, I feel it's probably a good idea to get, what's been done so far, live.

The big change is a major refactor to the async types and behaviours - lots of bug fixes and a project to standardise the Async types and methods. Because of this standardisation project (which is ongoing) you may see some compilation errors due to changes in method names or arguments (when using named arguments).

In practice this will mean some manual fix ups, but once done you shouldn't see any problems from the changes in behaviour.

There are lots of bug fixes in the async types, but also in general, so if you're one of those people that couldn't use the beta for whatever reason, I would highly recommend that you migrate to v3.0.0 - I've been using much of this in production for many months.

The kind of compilation issue you may see is:

    Match(Some, None)

Becoming variously:

    Match(SomeAsync, None)
    Match(Some, NoneAsync)
    Match(SomeAsync, NoneAsync)

This is to reduce the chance of compiler misunderstanding when using the many variants of each type of Match, Map, Bind etc.

Also, some functions may have changed from say Match to MatchAsync and vice versa depending on the context.

The scale of improvements in the async story can't be understated, there are many bug fixes, but also a huge increase in the amount of async functionality added. Not least from new types like EitherAsync, FunctorAsync, BiFunctorAsync, ApplicativeAsync, MonadAsync

I am now leveraging the type-class/class-instance system and splitting the synchronous and asynchronous types - this puts a very powerful type-safe story in place for the core types and their class instances. But this has also lead to an enormous amount of typing as the fall-out from this means essentially creating an Async type for each synchronous type (Option and OptionAsync for example). The end goal is a very happy place where there's a mathematical relationship between sync and async, and powerful features that fall out of it by default (like benefits in the higher-kinds system which allows pairs of monads to be composed into a single type).

This will be worked on over the next 6 months or so and will gradually improve.

A similar project has begun to create a type-safe way of describing safe and unsafe types. Unsafe types are those that will hold and/or return null. This is in an earlier stage than the async updates - but will have similarly profound effects for the standardisation of the core types and the relationship between safe and unsafe types (i.e. a safe type can become unsafe, but not the other way around).

A quick run down of what I could see from the commit notes. This is not an exhaustive list by any means:

  • Improvements in the type-class discovery system for Record equality, ordering, etc.
  • Additional class instances that derive from the Eq, Ord, Functor, Applicative, and Monad type-classes
  • Seq and Map various fixes and performance improvements
  • Significant improvements to OptionNone
  • Addition of EitherLeft<L> and EitherRight<R> to make type inference easier with Either<L, R>
  • IgnoreBaseAttribute for Record types - stops the base type fields being used for any Record operations
  • Minor performance improvements for Record types
  • Bug fixes in Result and OptionalResult
  • Try has partition, succs, and fails like Either's partition, lefts, and `rights.
  • Tuple<,,> and ValueTuple<,,> Sum fixes
  • Additional constructors and Prelude functions for the Writer monad
  • Massive improvements to the Higher Kinds feature to have more reliable binding operations which mean more robust pairing of monadic types. Also understand the pairings between async types and async and non-async types, which removes the chance of the .Result being called on an async type just to make it compatible in the HKT system. NOTE: This may mean some of the functions for the HKT types disappear - causing compilation errors. This is a good thing, they didn't do what you thought they did and you'll need to come up with a better solution yourself
  • Sequence and Traverse added for Writer, Reader, State, and RWS monads when they're paired with enumerable monads like Seq, Lst, IEnumerable, etc.

There's probably lots more, if you have a few days you can check the merge; it's probably not that fun to read.

Thanks to all contributors who helped put this release together. This is the first part of the bigger picture where async and unsafe become distinct typed concepts, rather than just suffixes on method names and types. Watch this space 👍

Major improvement: `Either` and `EitherUnsafe` construction without generic args!

05 Apr 01:57
84c0a7a
Compare
Choose a tag to compare

I don't normally do release notes for 'minor' releases, but even though this is a smallish change, it will have quite a profound effect on those of you using the Either, EitherUnsafe, and Option types.

Up until now the standard way to construct an Either would be to use one of the two constructor functions:

    var either = Left<string, int>("Failed");
    var either = Right<string, int>(100);

Typing those generic arguments in every time due to C#'s terrible type inference system is definitely one of the point points of this library (and C# in general).

Well, now I've solved it. You can now construct your Either types like so:

    var either = Left("failed");
    var either = Right(100);

Cue the collective sigh of relief!

I haven't managed to cheat C#, it still has its limitations: I have used a similar technique to None (which removes the need to use Option<int>.None (for example). None is a field in the Prelude which is of type OptionNone. The Option<A> type has implicit conversion operators to make use of it relatively seamless (which mostly removes the need to type in the generic argument type).

There are now two new types EitherRight<R> and EitherLeft<L> that work in a similar way to OptionNone. The main difference is that they both implement the subset of functions that are appropriate for the state they represent and you can use them in LINQ expressions without the type-system complaining.

For example:

   var r =from x in Right(2)
          from y in Right(123)
          from z in Right(5)
          select x + y + z;

The value r here is a EitherRight<R>.

With the example below there are both Either and EitherRight values in the LINQ expression.

    Either<string, int> either = Right(2);

    var r = from z in either
            from x in Right(123)
            from y in Right(5)
            select x + y + z;

The value r here is an Either<L, R>.

You can use any combination:

    Either<string, int> either = Right(2);

    var r = from x in Right(123)
            from y in Right(5)
            from z in either
            select x + y + z;

One thing to be wary of is when using Left(x) - because the R type can't be inferred it will always have a bound value of Unit:

    from x in Right(2)
    from y in Left("error")
    from z in Right(5)
    select x + y + z;

Here y is Unit and so you'll get a compile time error. This is unlikely to be a big issue in practice, but you can Bind the EitherLeft into an Either like so:

    from x in Right(2)
    from y in Left("error").Bind<int>()
    from z in Right(5)
    select x + y + z;

The reason that Bind doesn't take a function for its argument is that it would never run (because it's following the rules of the Either monad). So, you can consider Bind to simply be a cast operation for the R value that doesn't exist.

EitherRight has five Bind functions:

        Either<L, R> Bind<L>();
        EitherRight<B> Bind<B>(Func<R, EitherRight<B>> f);
        Either<L, R> Bind<L>(Func<R, EitherLeft<L>> f); 
        Either<L, B> Bind<L, B>(Func<R, Either<L, B>> f);
        EitherUnsafe<L, B> Bind<L, B>(Func<R, EitherUnsafe<L, B>> f);

The first allows for casting of the unknown L type, the next one allows for the Bind to continue on the Right branch, the next switches the state to Left (and in the process gives us enough information to construct an Either), the next two are the 'classic' bind operations that you'd see in `Either.

OptionNone has also been gifted with a new interface that supports the subset of behaviour that's appropriate for an Option in a None state (which, is very limited, obviously). But it also supports the Bind function to cast the type.

Note

  • All existing constructor functions will continue to work as-is, so this should have zero effect on any existing code.
  • I will be bringing this technique to the other types that have multiple type arguments soon

This has definitely been one of my biggest gripes over the years, so I hope you enjoy the release as much as I know I'm going to 👍

Traverse and Sequence for Writer, Reader, State, and RWS + Async updates

07 Feb 14:52
aa9d776
Compare
Choose a tag to compare

This is a minor release to bump the previous version up to beta, as it appears pretty stable. This may allow a few more people to use the new async features (and prepare for the breaking changes).

There are a few improvements and bug fixes here:

  • Traverse and Sequence now works for sequences of Reader, Writer, State, and RWS monads

These extensions are manually added rather than auto-implemented by the HKT system. So, the implementations of each are as optimal as possible, but because of the amount of typing - not all combinations are done. However, you should find that the combination of the monads listed above with Seq, Lst, Arr, [], HashSet, Set, Stck, and IEnumerable are all supported.

NOTE: The reverse operation isn't current implemented. I felt that the most useful operation would be to turn sequences of readers/writers/states, into a reader/writer/state where the bound value is a sequence (following the rules of the readers/writers/state monad). The other direction is clearly less useful (although will be implemented at some point I'm sure).

static Writer<MSeq<string>, Seq<string>, int> writer(int value, Seq<string> output) => () =>
    (value, output, false);

static Writer<MSeq<string>, Seq<string>, Seq<int>> multWithLog(Seq<int> input) =>
    from _ in writer(0, SeqOne("Start"))
    let c = input.Map(i => writer(i * 10, SeqOne($"Number: {i}")))
    from r in c.Sequence()
    select r;

/ Run the writer
var res = multWithLog(Seq(1, 2, 3)).Run();

// res.Value.IfNoneOrFail(Seq<int>()) == Seq(10, 20, 30);
// res.Output == Seq("Start", "Number: 1", "Number: 2", "Number: 3");
  • Fix for: Result<>.Bottom.IsBottom is false - #341
  • Various minor improvements

On nu-get now