IO refactor #1428
louthy
announced in
Announcements
IO refactor
#1428
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
IO
I have refactored the
IO<A>
monad, which is used to underpin all side-effects in language-ext. The API surface is unchanged, but the inner workings have been substantially refactored. Instead of the four concrete implementations of the abstractIO<A>
type (IOPure<A>
,IOFail<A>
,IOSync<A>
, andIOAsync<A>
), there is now a 'DSL' of operations deriving fromIO<A>
which are interpreted in theRun
andRunAsync
methods (it now works like a Free monad).The DSL is also extensible, so if you have some behaviours you'd like to embed into the IO interpreter than you can derive from one of these four types.
The benefits of the refactored approach are:
The last one is a big win. Previously, infinite recursion only worked in certain scenarios. It should work for all scenarios now.
Take a look at this infinite-loop sample:
The second
from
expression recursively callsinfinite
which would usually blow the stack. However, now the stack will not blow and this example would run forever*.*All LINQ expressions require a trailing
select ...
, that is technically something that needs to be invoked after each recursive call toinfinite
. Therefore, with the above implementation, we get a space-leak (memory is consumed for each loop).To avoid that when using recursion in LINQ, you can use the
tail
function:What
tail
does is says "We're not going run theselect
at all, we'll just return the result ofinfinite
". That means we don't have to keep track of any continuations in memory. It also means you should never do extra processing in theselect
, just return ther
as-is and everything will work: infinite recursion without space leaks.Not doing work after a tail-call is a limitation of tail-recursion in every language that supports it. So, I'm OK being explicit about it with LINQ. Just be careful to not do any additional processing or changing of types in the
select
.Note, if you don't use LINQ and instead use a regular monad-bind operation, then we don't need the
tail
call at all:That will run without blowing the stack and without space-leaks. Below is a chart of memory-usage after 670 million iterations:
What's nice about looking a the memory-graph is that, not only is it flat in terms of total-usage (around
26mb
), it only ever uses theGen 0
heap. This is something I've always said about functional-programming. We may we generate a lot of temporary objects (lambdas and the like), but they rarely live long enough to cause memory pressures in higher generations of the heap. Even though this is a very simple sample and you wouldn't expect that much pressure, the benefits of most of your memory usage being inGen 0
is that you're likely using memory addresses already cached by the CPU -- so the churn of objects is less problematic that is often posited.StreamT
made experimentalI'm not sure how I'm going to fix this issue, so until I have a good idea,
StreamT
will be marked as[Experimental]
. To use it, add this to the top of a file:#pragma warning disable LX_StreamT
Conclusion
This was a pretty large change, so if you're using the
beta
in production code, please be wary of this release. And, if you spot any issues, please let me know.This discussion was created from the release IO refactor.
Beta Was this translation helpful? Give feedback.
All reactions