Skip to content

IO refactor continued

Pre-release
Pre-release
Compare
Choose a tag to compare
@louthy louthy released this 23 Dec 21:41
· 35 commits to main since this release

IO<A>

The work has continued following on from the last IO refactor release. The previous release was less about optimisation and more about correctness, this release is all about making the async/await state-machines disappear for synchronous operations.

  • The core Run and RunAsync methods have been updated to never await. If at any point an asynchronous DSL entry is encountered then processing is deferred to RunAsyncInternal (which does use await). Because, RunAsync uses ValueTask it's possible to run synchronous processes with next to zero overhead and still resolve to a fully asynchronous expression when one is encountered.
  • The DSL types have all been updated too, to try to run synchronously, if possible, and if not defer to asynchronous versions.
  • DSL state-machine support for resource tracking. It automatically disposes resources on exception-throw
  • DSL support for three folding types: IOFold, IOFoldWhile, IOFoldUntil (see 'Folding')
  • DSL Support for Final<F> (see Final<F>)

TODO

  • DSL support for Repeat* and Retry* - then all core capabilities can run synchronously if they are composed entirely of synchronous components.

Folding

The standard FoldWhile and FoldUntil behaviour has changed for IO (and will change for all FoldWhile and FoldUntil eventually: it dawned on me that it was a bit of a waste that FoldWhile was equivalent to FoldUntil but with a not on the predicate.

So, the change in behaviour is:

  • The FoldUntil predicate test (and potential return) is run after the fold-delegate has been run (so it gets the current-value + the fold-delegate updated-state).
    • This was the previous behaviour
  • The FoldWhile predicate test (and potential return) is run before the fold-delegate is run (so it gets the current-value + the current-state).

The benefit of this approach is that you can stop a fold-operation running if the state is already 'bad' with FoldWhile, whereas with FoldUntil you can exit once the fold-operation makes the state 'bad'. The difference is subtle, but it does give additional options.

Final<F> trait

Final<F> is a new trait-type to support try / finally behaviour. This has been implemented for IO for now. This will expand out to other types later. You can see from the updated implementation of Bracket how this works:

public IO<C> Bracket<B, C>(Func<A, IO<C>> Use, Func<Error, IO<C>> Catch, Func<A, IO<B>> Fin) =>
    Bind(x => Use(x).Catch(Catch).Finally(Fin(x)));

It's still early for this type, but I expect to provide @finally methods that work a bit like the @catch methods.

Unit tests for IO

One of the things that's holding up the full-release of v5 (other than the outstanding bugs in Pipes) is the lack of unit-tests for all of the new functionality. So, I've experimented using Rider's AI assistant to help me write the unit-tests. It's fair to say that it's not too smart, but at least it wrote a lot of the boilerplate. So, once I'd fixed up the errors, it was quite useful.

It's debatable whether it was much quicker or not. But, I haven't really spent any time with AI assistants, so I guess it might just be my inexperience of prompting them. I think it's worth pursuing to see if it can help me get through the unit-tests that are needed for v5.