-
-
Notifications
You must be signed in to change notification settings - Fork 422
Frequently Asked Questions
- "Why don't you just use F#?"
- Why are you using
camelCase
names? Microsoft says "no" - What's your problem with async?
- Where is
Result
? - Where is
OptionAysnc
,EitherAsync
, andTryAsync
? - Where have the
NewType
,NumType
, andFloatType
gone? - Where are the
LanguagExt.Transformers
extensions? - Why doesn't
LanguageExt.CodeGen
work? - Why doesn't type X serialise/deserialise?
"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.
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:
- Drastically reduce the amount of code I'd have to write
- 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 ofEither<L, R>
-
FinT<M, A>
- transformer version ofFin<A>
-
OptionT<M, A>
- transformer version ofOption<A>
-
TryT<M, A>
- transformer version ofTry<A>
-
ValidationT<F, M, A>
- transformer version ofValidation<F, A>
-
ContT<R, M, A>
- continuations -
IdentityT<M, A>
- identity transformer, does nothing but lift anM
-
ReaderT<E, M, A>
- transformer version ofReader<E, A>
-
WriterT<W, M, A>
- transformer version ofWriter<W, A>
-
StateT<S, M, A>
- transformer version ofState<S, A>
-
RWST<R, W, S, M, A>
- combines the effects ofReader
,Writer
,State
, andM
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.
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 calledFin<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.
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.
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.
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.
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.
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.