Skip to content

Frequently Asked Questions

Paul Louth edited this page Oct 29, 2024 · 26 revisions

Why are you using camelCase names?

"Microsoft says 'no'" is something that the C# community should get over. Microsoft consistently make mistakes, just look at how many failed web-frameworks we've had over the years! They shouldn't be seen as the arbiter of how code should be written.

Of course there are also community norms for writing C# code and using camelCase isn't a norm. So why have I done it?

Primarily, it's because camelCase names look better in LINQ expressions. And because this library is all about pure-functional programming, we expect to use LINQ, pretty much all of the time. Take a look at the CardGame sample to get an idea of what I am talking about. When your entire application is LINQ then the camelCase names come into their own and the code starts to look more like an ML language, like F# or Haskell.

So, it's entirely opinionated, and it may sting, but this library isn't about pandering to norms. It's here to rip up the rule book and create a sub-community within the wider C# community that want to do it differently.

What's your problem with async?

I have no issue with async code, my biggest issue is how - the moment you use async - it colours your code in such a way that it makes it not compose with synchronous code and requires language features (rather than classic composition) to leverage.

What this leads to (for a library like this) is a doubling up of every single type and nearly every function that accepts a Func as an argument (which is many of them). So, as well as Option, you need OptionAsync, as well as Map, you need MapAsync. This is ridiculous.

So, in version 5 of language-ext I decided to take a stand and sort it out in a way that would:

  1. Drastically reduce the amount of code I'd have to write
  2. Lead to a more powerful solution for users of the library

Pretty much every *Async variant has been dropped. There is now just one type that does asynchronous code: IO<A>. It can lift both synchronous and asynchronous computations (via IO.lift(f) and IO.liftAsync(f)). Once you have an IO<A>, you can lift that into a monad-transformer stack.

For example, instead of OptionAsync<A> you can now use:

    OptionT<IO, A>

At the time of writing, these are the available monad-transformers:

  • Proxy<UOut, UIn, DIn, DOut, M, A> (the base-type of the Pipes system)
  • StreamT<M, A> - streams lazy effects
  • EitherT<L, M, R> - transformer version of Either<L, R>
  • FinT<M, A> - transformer version of Fin<A>
  • OptionT<M, A> - transformer version of Option<A>
  • TryT<M, A> - transformer version of Try<A>
  • ValidationT<F, M, A> - transformer version of Validation<F, A>
  • ContT<R, M, A> - continuations
  • IdentityT<M, A> - identity transformer, does nothing but lift an M
  • ReaderT<E, M, A> - transformer version of Reader<E, A>
  • WriterT<W, M, A> - transformer version of Writer<W, A>
  • StateT<S, M, A> - transformer version of State<S, A>
  • RWST<R, W, S, M, A> - combines the effects of Reader, Writer, State, and M into a single type. In theory not necessary, but it performs better than stacking those effects manually.

You can stack multiple transformers with any monad you like (so, not just IO<A>) to create 'super-monads' which are the sum of their parts.

All of this means we have a much, much more powerful system for asynchronous code than before (in v4), but you must use the IO<A> type as the foundation. It composes in a way that Task can't and works for all IO side-effects, not just asynchronous ones.

Where is Result?

Result was removed from v5 of language-ext because there was constant confusion about its role. Result was originally intended simply as an intermediate type for the Try* lambda-based types and not for public consumption. Unfortunately most people didn't read the documentation and used it thinking it had the same status as Either, Option, etc.

The options were to either keep the type and develop it fully or remove it completely.

The reasons for removal are:

  • I don't like naming commonly used types in a way that is likely to clash with other common libraries (which is why Result was originally parked in another namespace).
    • Result is such a common name that name clashes are likely
  • Another type already does what everybody wants Result to do. It's called Fin<A> and was named based on the French for 'end', 'finished', 'final', ... i.e. Result.
    • This name that is unlikely to clash and is happily slightly less typing.

Where is OptionAsync, EitherAsync, and TryAsync?

The decision was made to reject async and the way it colours code in such a way that most APIs need two versions of every function/method. Instead, there is a single home for asynchronous code and that is the IO<A> monad. In some ways you can consider IO<A> to be a more advanced Task<A>. IO<A> doesn't litter the code-base with *Async variants. It supports both synchronous and asynchronous computations and is specifically engineered to represent impure/IO side-effects (which Task always are in one way or another).

Additionally, monad-transformers are now a feature of language-ext, which means you can lift the IO monad into any monad-transformer to augment its capabilities. So, if you want OptionAsync<A> then use OptionT<IO, A>, if you want EitherAsync<L, R> then use EitherT<L, IO, R>, if you want TryAsync<A>, use TryT<IO, A>.

The benefit of this new approach is that you can make anything async. You don't have to wait for me to write a *Async variant. For example, there was never a ValidationAsync<F, A> pairing for Validation<F, A>. Now you can create your own from ValidationT<F, IO, A>. Nor was there a FinAsync<A>, but now you can use FinT<IO, A>.

And because other types leverage the IO<A> monad, like Eff<A> and Eff<RT, A>. You can also lift those: OptionT<Eff, A>. That makes an optional-effect that can also do async.

So, the change, although painful if you've used the *Async types, brings in a ton of new capabilities.

If it's especially painful for your code-base, then remember you can build your own OptionAsync<A> by building a wrapper for OptionT<IO, A>. So, you could build a polyfil to make migration easier.

Where have the NewType, NumType, and FloatType gone?

To a certain extent they have been superseded by record. We can now create an alias type by doing this:

record MyAlias(int Value);

Which is much more elegant. So, that is primarily why NewType (and variants) have been removed.

However, there's a new set of traits that can help build 'domain types' that fit into a subset of shapes, based on ideas from this article.

Example usage can be seen in the DomainTypesExamples sample.

Where are the LanguagExt.Transformers extensions?

Language-Ext version 5 introduced Higher-Kinded Traits. These traits allow us to build real monad-transformers using the type-system as-is. No hacks are needed and we now don't need to generate 500,000 lines of extension methods. So, LanguageExt.Transformers have been removed.

See Paul Louths's blog for an introduction.

Why doesn't LanguageExt.CodeGen work?

Officially, it is now deprecated and unsupported. There are future plans to add a new source-generators based project, but that doesn't exist yet.

The library that CodeGen was based on has now been deprecated (since Source Generators became an official MS feature). That makes it next to impossible for me to continue supporting it.

It's probably possible to get it working, but you'll need to install .NET SDK version 2.1.818.

The new traits for Functor, Applicative, Monad, etc. are meant to help in the implementation of functional-types in the way that could only be done with code-gen in the past. So, the advice is to use the new traits system. Paul Louth's Higher Kinds in C# series can help guide you.

Why doesn't type X serialise/deserialise?

Serialisation (or more specifically, deserialisation) has been a thorn in my side for as long as this library has existed. I don't want to include dependencies on Newtonsoft.Json, System.Json, or any serialisation library. And so, those libraries have to be able to infer the shape of the types without my help.

The ever changing nature of those libraries has made creating a robust long-term solution impossible. Over the years I've tried various uses of DataMember and other attributes, various coercion tactics (like making Option enumerable), etc. to try and convince those libraries to make good decisions, but they don't. And so, officially, serialisation and deserialisation is not supported by the Core library.

In the roadmap are some projects that will allow for effective serialisation/deserialisation in the future. But right now they don't exist. And 2024 has been extremely busy trying to get the foundations of v5 correct, so it's likely to be some time in 2025 now.

Workarounds for this are:

  • Transform values of type X into something that the serialisation libraries can handle.
  • Write bespoke handlers that work with the serialisation/deserialisation library of your choice.