Replies: 10 comments 18 replies
-
Recently, I was stuck at combining Aff and State monads, and found that it's currently possible, but tricky to implement, so I decided to wait a while for the new release of language-ext to implement desired behaviour easier. I was hoping that your Transducers project will achieve the stuff you was trying to succeed (I guess it was about stacking monads?), but unfortunately, I guess C#'s lacks of some type-system stuff can't be tricked. I'm not experienced enough to give any advices about library maintenance, but maybe it would be good if you split it in two parts: for example, obsolete language-ext 4.* with occasional long-time support (to complete all sync/async variants at least) and language-ext 5.* with the new way of working with Effects. It's like we have .NET(Core) and .Net Framework right now. By the way, there was a discussion opened in F# repo yesterday, where they are talking about similar breaking/incompatible changes problem, maybe this can help you to make a descision?
Noticed that too, but I'm not sure C# team will change its priorities from backward compatability to new (possibly breaking) changes and features. Also, if they will: how long will it take? Remember how long C# community waits for HKT or at least DUs support? As I understand, even F# team is afraid of making any steps further due to unforseen C# changes that may break something (recent was abstract static methods in interfaces; next one is possibly upcoming C#'s DU implementation), so they will need to change their plans and implementions to support interop with C#. And I'm not even saying about its type-system that still has some internal bugs that were done so much long ago, that it would be difficult to fix it now (IIRC it was something about with Nullable struct and its recursive field). I'm not blaming C# team - its just how they have to deal with .NET history. |
Beta Was this translation helpful? Give feedback.
-
One thing that annoyed me about .NET Core was how Microsoft just left half of the .NET community behind. They didn't have to do that and it still has repercussions today. I think that whole process was incredibly poorly managed. I'm not looking to create two versions. There already is an area of language-ext for deprecated/obsolete code, that will just expand and I'll move the old code there so it doesn't pollute the API documentation. Old code should still compile fine with an updated language-ext, it just will have lots of
That's exactly what I am solving. In the prototype is an example transformer stack: public static class Application<Env>
{
public static Application<Env, A> Success<A>(A value) =>
new (EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A>.Right(value));
public static Application<Env, A> Fail<A>(Error value) =>
new (EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A>.Left(value));
public static Application<Env, A> Lift<A>(ReaderT<MIO<Env>, Env, A> value) =>
new (EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A>.Lift(value.Morphism));
public static readonly Application<Env, Env> Ask =
Lift(ReaderT<MIO<Env>, Env>.Ask);
}
public record Application<Env, A>(EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A> Transformer)
{
public static Application<Env, A> Pure(A value) =>
new (EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A>.Right(value));
public Application<Env, B> Map<B>(Func<A, B> f) =>
new (Transformer.MapRight(f));
public Application<Env, B> Select<B>(Func<A, B> f) =>
new (Transformer.MapRight(f));
public Application<Env, B> Bind<B>(Func<A, Application<Env, B>> f) =>
new (Transformer.Bind(x => f(x).Transformer));
public Application<Env, C> SelectMany<B, C>(Func<A, Application<Env, B>> bind, Func<A, B, C> f) =>
Bind(x => bind(x).Map(y => f(x, y)));
public Application<Env, B> BiMap<B>(Func<Error, Error> Fail, Func<A, B> Succ, Func<B>? Bottom = null) =>
new(Transformer.BiMap(Fail, Succ));
} This example is similar to the kind of monad-transformer stacks one might build in a Haskell project. To the untrained eye it might not be obvious what's going on. The type EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A> Transformer
What you're looking at there is a monad-transformer stack that combines the behaviours of public static Application<string, string> Example =>
from x in Application<string>.Success(100)
from y in Application<string>.Success(200)
from n in Application<string>.Ask
select $"Hello {n}, the answer is: {x + y}"; And obviously any other monad-transformers that are built can be stacked in the same way. The stacking order is actually the reverse of how they're compose, so: So, this: EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A> is stacked like this:
Which is the same as how they work in Haskell. If you look at the static public static Application<Env, A> Success<A>(A value) =>
new (EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A>.Right(value));
public static Application<Env, A> Fail<A>(Error value) =>
new (EitherT<MReaderT<MIO<Env>, Env>, Env, Error, A>.Left(value)); and public static readonly Application<Env, Env> Ask =
Lift(ReaderT<MIO<Env>, Env>.Ask); To make its own bespoke I've been trying lots of different combinations of how these can be composed. I'm not 100% settled on the best approach, but I'm getting close. Writing this code is quite hard if you don't know what you're doing, which is why I want this to be generated using Source Generators (well, as much as possible). Obviously I'll also built all the monad transformer types like |
Beta Was this translation helpful? Give feedback.
-
Dead-lockWill there be a Here is the example code, that I tested: private void Button_Click(object sender, RoutedEventArgs e)
{
Run(_ => DelayReturn("hello"), default);
}
private async Task<T> DelayReturn<T>(T value)
{
await Task.Delay(1000);
return value;
} |
Beta Was this translation helpful? Give feedback.
-
Just to add my two cents... I would welcome the change, as I recently encountered difficulties when combining synchronous and asynchronous methods with LanguageExt. At the same time, I must confess that my codebase affected by this issue is not extensive, so it's probably easier for me to advocate for the change. Essentially, I believe that from a neutral standpoint, it's a no-brainer to appreciate the simplicity of dealing with an |
Beta Was this translation helpful? Give feedback.
-
Proposed change sounds great! I currently use BTW: Here is another interesting example of |
Beta Was this translation helpful? Give feedback.
-
For anyone who's interested in following the progress of this, it's all in the I've written a lot of code in the past few weeks and not run any of it, so there's plenty of testing I need to do. But just thought I'd test out one of the major benefits of the This example shows that in action: static void Main(string[] args) =>
infiniteLoop(1).Run(new MinimalRT());
static IO<MinimalRT, Error, Unit> infiniteLoop(int value) =>
from _ in writeLine($"{value}")
from r in infiniteLoop(value + 1)
select unit; This will loop forever, yet the stack never gets more than about 10 functions deep at any one time. This is a major feature that I'm very excited about, because this is a really common way that functional developers write their code and can lead to much more elegant solutions. |
Beta Was this translation helpful? Give feedback.
-
|
Beta Was this translation helpful? Give feedback.
-
@timmi-on-rails re: cross-thread synchronisation... I have added support for the internal state of the transducers to track the That means the This then supports the new The final outputted value of wrapped transducer is then passed back to the original I am yet to fully test this, but it should mean that running anything on the UI thread will just mean wrapping your from x in regularThreadOp1
from r in post(uiThreadOp1)
from y in regularThreadOp2
select x + r + y; Open to thoughts on this approach. |
Beta Was this translation helpful? Give feedback.
-
There's a new WPF test-app in the The app features a button and a timer. The timer updates the text of the button every millisecond. Clicking on the button resets the text. It tests the The timer is an infinite loop that updates the count, sets the button-text, and sleeps the thread until the next tick... IO<MinimalRT, Error, Unit> tickIO =>
from _1 in modifyCount(x => x + 1)
from _2 in post(setButtonText(CounterButton, $"{count}"))
from _3 in waitFor(1)
from _4 in tail(tickIO)
select unit; You'll notice that if you drag the window around, it doesn't lock up, so the sleep-delay is not on the UI thread; but it also updates the button, which must be on the UI thread. So that proves it all works nicely.
IO<MinimalRT, Error, Unit> waitFor(double ms) =>
liftIO(async token => await Task.Delay(TimeSpan.FromMilliseconds(ms), token));
The // Start the ticking...
ignore(tickIO.RunAsync(runtime)); The button-click resets the value and updates the button: IO<MinimalRT, Error, Unit> buttonOnClickIO =>
from _1 in resetCount
from _2 in post(setButtonText(CounterButton, $"{count}"))
select unit; The Window button click handler launches the async void ButtonOnClick(object? sender, RoutedEventArgs e) =>
ignore(await buttonOnClickIO.RunAsync(runtime)); The /// <summary>
/// Helper IO for setting button text
/// </summary>
static IO<MinimalRT, Error, Unit> setButtonText(Button button, string text) =>
lift(action: () => button.Content = text); But, obviously calling this without wrapping the call in So, what do we see in this demo:
Quite a lot for a small demo 👍 |
Beta Was this translation helpful? Give feedback.
-
Excuse, me. Could you write code considering Microsoft's best practices? |
Beta Was this translation helpful? Give feedback.
-
Problem
Right now language-ext has
Async
variants of many of the types:Option
OptionAsync
Either
EitherAsync
Try
TryAsync
TryOption
TryOptionAsync
Eff
Aff
And some types haven't yet gained either an async or a sync version:
Reader
Writer
State
RWS
Fin
Validation
Effect
Pipe
Consumer
Producer
Having to create an
*Async
variant for every type is an unreal amount of typing and opens up many opportunities for bugs. The types in this library are all designed to work with each other also, which means lots ofToAsync
and LINQ operator extensions to be written.If I'm honest, I'm kinda sick of it, because this is all brought about by the
async / await
machinery of C# which 'colours' your code.Also, really I probably shouldn't have ever built
OptionAsync
andEitherAsync
; I fell into the trap of trying to support the C# async machinery for types that are pure data types. Also, because C# doesn't support higher-kinded generics, it meant stacking IO and data monads was impossible - so we got these variants. Right nowEff
covers a lot of this functionality.There's a big debate going on in language-development-land on whether function colouring makes sense for async code. Especially as the
async / await
machinery is designed to make it easy to sequence concurrent operations. If the machinery existed when C# was invented then perhaps we wouldn't haveasync / await
, but instead have afork
operator to launch a task without waiting for its result (likefork
does inAff
).The primary argument for (async/await):
The argument against:
Main
Languages like Go have no concept of async and sync (from what I understand), if an operation is IO then the thread will be released until the IO is complete. Haskell has the
IO
monad which can do both sync and async operations without any declarative component (it also has afork
operator).My Transducers prototype - which is informing how I'm going to refactor some of the core types in language-ext - has been caught in the same trap, but it's even worse in Transducers land (for boring reasons that I won't go into right now).
Even the C# team are seemingly regretting their own
async / await
implementation because they've been investigating adding 'green threads' to C#. They gave up for various technical reasons (see the link), not because green threads is necessarily a bad idea.Proposal
*Async
variants of data monads (OptionAsync
andEitherAsync
)IO<A>
monad that is for side-effecting computations, both synchronous and asynchronousEff
monad into a more 'heavyweight' IO monad that combines resource tracking, error handling, state management, and both synchronous and asynchronous computations.Either
andIO
so analogues toEitherAsync
etc. can be user-created (will also be used to create theEff
monad)Aff
,Try
,TryAsync
,TryOption
,TryOptionAsync
- encourage switch toEff
(or transformers)Most usages of
async/await
are for concurrent IO code not for launching highly-parallel CPU threaded code. So, the focus ofEff
and theIO
monads should be on making that efficient and relying onfork
to launch threads for CPU bound parallel operations.Thoughts:
This sounds like a breaking change, how bad will this be?
This will force users to use the
IO
orEff
type to do IO, which feels right, but could lead to some major refactoring of existing code-bases. I'd want to limit that as much as possible, but in many cases it will lead to changing types from:Aff
,OptionAsync
,EitherAsync
,Try
,TryAsync
,TryOption
, andTryOptionAsync
toEff
or monad transformer stacks. Not free, but quite mechanical. It will hurt somewhat though, this change is too large not to have an impact.Type mapping:
Aff<RT, A>
Eff<RT, A>
Aff<A>
Eff<A>
OptionAsync<A>
OptionT<IO, A>
EitherAsync<E, A>
EitherT<E, IO, A>
Try<A>
TryT<IO, A>
TryAsync<A>
TryT<IO, A>
TryOption<A>
TryT<OptionT<IO>, A>
TryOptionAsync<A>
TryT<OptionT<IO>, A>
If there's no
async / await
, is this blocking?No. The key thing to realise is that behind the scenes a lot of the async machinery is implemented with synchronous looking code. Either through wait-handles or spinning & yielding.
I propose that because
IO
andEff
will be for IO that we could use theSpinWait
technique that's used in things likeConcurrentDictionary
. and parts of theTask
internals:When you look at the internals of
SpinOnce
you'll see it's quite interesting (the name isn't descriptive at all!):That code is pretty hard to follow,
SpinWait.WaitOne
performs CPU-intensive spinning for 10 iterations before yielding. However, it doesn’t return to the caller immediately after each of those cycles: instead, it callsThread.SpinWait
to spin via the CLR (and ultimately the operating system) for a set time period. This time period is initially a few tens of nanoseconds, but doubles with each iteration until the 10 iterations are up. This ensures some predictability in the total time spent in the CPU-intensive spinning phase, which the CLR and operating system can tune according to conditions. Typically, it’s in the few-tens-of-microseconds region — small, but more than the cost of a context switch. On a single-core machine,SpinWait
yields on every iteration.If a
SpinWait
remains in “spin-yielding” mode for long enough (maybe 20 cycles) it will periodically yield for a few milliseconds to further save resources and help other threads progress.That means - for IO operations that are fast - we won't have switched the context for the current thread and therefore we won't lose time context-switching. This can be highly effective as context-switching too early (just calling
Thread.Yield
in a spin-loop) will cost over 4000+ cycles per-yield and will lose even more time from the ensuing cache effects. For IO that does take a long time we go into aSleep
/Yield
loop where we allow other tasks on this thread to take over and use the CPU resource while we're busy.This should work pretty well for cooperative/concurrent IO operations, although it might be a little primitive. This technique will always run the continuation on the current thread, which I think for modern multi-core CPUs is probably a much better approach. But, if true deferment to another thread is needed then the
Eff
orIO
monads can be forked which will schedule the task for another thread.The benefit of this is that you can run code that looks like synchronous code, but it will still yield time to the task-scheduler working on the current thread (in the same way that
async / await
does):Obviously, all of this will be wrapped up inside the
IO
andEff
monads. So these moving parts won't be exposed. You will just write code as though it's synchronous and let language-ext deal with the concurrency. And if you need parallelism, justfork
yourEff
orIO
computations.Conclusion
This will lead to:
Eff
andIO
as the primary IO monadsHowever, it comes with some refactoring costs (and initially lots of
[Obsolete]
warnings).I want to do this, but I realise this is a biggy, so please have your say. I'm interested in support, dissent, alternative approaches, and ideas.
Beta Was this translation helpful? Give feedback.
All reactions