New features: Monadic action operators, StreamT, and Iterable #1349
louthy
announced in
Announcements
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
-
Features:
IterablemonadStreamTmonad-transformer|AtomrationalisationFoldOptionAsynchelperIAsyncEnumerableLINQ extensionsMonadic action operators
The monadic action operator
>>allow the chaining of two monadic actions together (like a regular bind operation), but we discard the result of the first.A good example of why we want this is the LINQ discards that end up looking like BASIC:
We are always discarding the result because each operation is a side-effecting IO and/or state operation.
Instead, we can now use the monadic action operator:
Here's another example:
In the above example you could just write:
It's really down to taste. I like things to line up!
Because operators can't have generics, we can only combine operands where the types are all available. For example:
But, we can de-abstract the
Kversions:And, also do quite a neat trick with
Unit:That propagates the result from the first operation, runs the second (unit returning) operation, and then returns the first-result. This is actually incredibly useful, I find.
Because, it's not completely general case, there will be times when your types don't line up, but it's definitely useful enough, and can drastically reduce the amount of numbered-discards! I also realise some might not like the repurposing of the shift-operator, but I chose that because it's the same operator used for the same purpose in Haskell. Another option may have been to use
&, which would be more flexible, but in my mind, less elegant. I'm happy to take soundings on this.The
CardGame samplehas more examples.New
IterablemonadThe
EnumerableMtype that was a wrapper forIEnumerable(that enabled traits like foldable, traversable, etc.) is nowIterable. It's now more advanced than the simple wrapper that existed before. You canAddan item to anIterable, or prepend an item withConsand it won't force a re-evaluation of the lazy sequence, which I think is pretty cool. The same is true for concatenation.Lots of the
AsEnumerablehave been renamed toAsIterable(I'll probably addAsEnumerable()back later (to returnIEnumerableagain). Just haven't gotten around to it yet, so watch out for compilation failures due to missingAsEnumerable.The type is relatively young, but is already has lots of features that
IEnumerbledoesn't.New
StreamTmonad-transformerIf lists are monads (
Seq<A>,Lst<A>,Iterable<A>, etc.) then why can't we have list monad-transformers? Well, we can, and that'sStreamT. For those that knowListTfrom Haskell, it's considered to be done wrong. It is formulated like this:So, the lifted monad wraps the collection. This has problems because it's not associative, which is one of the rules of monads. It also feels instinctively the wrong way around. Do we want a single effect that evaluates to a collection, or do we want a collection of effects? I'd argue a collection of effects is much more useful, if each entry in a collection can run an IO operation then we have streams.
So, we want something like this:
It's easy to see how that leads to reactive event systems and the like.
Anyway, that's what
StreamTis, it'sListTdone right.Here's a simple example of
IObeing lifted intoStreamT:So,
naturalsis an infinite lazy stream (well, up tolong.MaxValue). Theexamplecomputation iterates every item innaturals, but it uses thewhereclause to decide what to let through to the rest of the expression. So,where v % 10000means we only let through every 10,000th value. We then callConsole.writeLineto put that number to the screen and finally, we dowhere falsewhich forces the continuation of the stream.The output looks like this:
That
where falsemight seem weird at first, but if it wasn't there, then we would exit the computation after the first item.falseis essentially saying "don't let anything thorugh" andselectis saying "we're done". So, if we never get to theselectthen we'll keep streaming the values (and running thewriteLineside effect).We can also lift
IAsyncEnumerablecollections into aStreamT(although you must have anIOmonad at the base of the transformer stack -- it needs this to get the cancellation token).We can also fold and yield the folded states as its own stream:
Here,
FoldUntilwill take each number in the stream and sum it. In its predicate it returnstrueevery 10th item. We then write the state to the console. The output looks like so:Support for recursive IO with zero space leaks
I have run the first
StreamTexample (that printed every 10,00th entry forever) to the point that this has counted over 4 billion. The internal implementation is recursive, so normally we'd expect a stack-overflow, but for liftedIOthere's a special trampoline in there that allows it to recurse forever (without space leaks either). What this means is we can use it for long lived event streams without worrying about memory leaks or stack-overflows.To an extent I see
StreamTas a much simpler pipes system. It doesn't have all of the features of pipes, but it is much, much easier to use.To see more examples, there's a 'Streams' project in the
Samplesfolder.Typed operators for
|I've added lots of operators for
|that keeps the.As()away when doing failure coalescing with the core types.AtomrationalisationI've simplified the
Atomtype:Swapfunctions (so, noSwapEff, or the like).Swapdoesn't return anOptionany more. This was only needed for atoms with validators. Instead, if a validator fails then we just return the original unchanged item. You can still use theChangedevent to see if an actual change has happened. This makes working with atoms a bit more elegant.Preludefunctions for using atoms withIO:atomIOto construct an atomswapIOto swap an item in an atom while in an IO monadvalueIOto access a snapshot of theAtomwriteIOto overwrite the value in theAtom(should be used with care as the update is not based on the previous value)FoldOptionNew
FoldOptionandFoldBackOptionfunctions for theFoldabletrait. These are likeFoldUntil, but instead of a predicate function to test for the end of the fold, the folder function itself can return anOption. IfNonethe fold ends with the latest state.AsynchelperAsync.await(Task<A>)- turns aTaskinto a synchronous process. This is a little bit likeTask.Resultbut without the baggage. The idea here is that you'd use it where you're already in an IO operation, or something that is within its own asynchronous state, to pass a value to a method that doesn't acceptTask.Async.fork(Func<A>, TimeSpan)andAsync.fork(Func<Task<A>>, TimeSpan)- both return aForkIOand allow for launching parallel effects that you can await later or cancel.I see these as potential stop-gap functions for those moving from
v4tov5where they might find an*Asyncvariant of a method has disappeared (likeMatchAsync, but can't at that point refactor everything). I'm still in two minds about this, but I'll leave them in for now.IAsyncEnumerableLINQ extensionsThe BCL doesn't support
Select,SelectMany,Where, etc. forIAsyncEnumerable. Well, now we do. I've only done a handful so far, but it's the extensions that matter.This discussion was created from the release New features: Monadic action operators, StreamT, and Iterable.
Beta Was this translation helpful? Give feedback.
All reactions