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
-
Features:
Iterable
monadStreamT
monad-transformer|
Atom
rationalisationFoldOption
Async
helperIAsyncEnumerable
LINQ 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
K
versions: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 sample
has more examples.New
Iterable
monadThe
EnumerableM
type 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 canAdd
an item to anIterable
, or prepend an item withCons
and 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
AsEnumerable
have been renamed toAsIterable
(I'll probably addAsEnumerable()
back later (to returnIEnumerable
again). 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
IEnumerble
doesn't.New
StreamT
monad-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 knowListT
from 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
StreamT
is, it'sListT
done right.Here's a simple example of
IO
being lifted intoStreamT
:So,
naturals
is an infinite lazy stream (well, up tolong.MaxValue
). Theexample
computation iterates every item innaturals
, but it uses thewhere
clause to decide what to let through to the rest of the expression. So,where v % 10000
means we only let through every 10,000th value. We then callConsole.writeLine
to put that number to the screen and finally, we dowhere false
which forces the continuation of the stream.The output looks like this:
That
where false
might seem weird at first, but if it wasn't there, then we would exit the computation after the first item.false
is essentially saying "don't let anything thorugh" andselect
is saying "we're done". So, if we never get to theselect
then we'll keep streaming the values (and running thewriteLine
side effect).We can also lift
IAsyncEnumerable
collections into aStreamT
(although you must have anIO
monad 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,
FoldUntil
will take each number in the stream and sum it. In its predicate it returnstrue
every 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
StreamT
example (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 liftedIO
there'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
StreamT
as 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
Samples
folder.Typed operators for
|
I've added lots of operators for
|
that keeps the.As()
away when doing failure coalescing with the core types.Atom
rationalisationI've simplified the
Atom
type:Swap
functions (so, noSwapEff
, or the like).Swap
doesn't return anOption
any 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 theChanged
event to see if an actual change has happened. This makes working with atoms a bit more elegant.Prelude
functions for using atoms withIO
:atomIO
to construct an atomswapIO
to swap an item in an atom while in an IO monadvalueIO
to access a snapshot of theAtom
writeIO
to overwrite the value in theAtom
(should be used with care as the update is not based on the previous value)FoldOption
New
FoldOption
andFoldBackOption
functions for theFoldable
trait. These are likeFoldUntil
, but instead of a predicate function to test for the end of the fold, the folder function itself can return anOption
. IfNone
the fold ends with the latest state.Async
helperAsync.await(Task<A>)
- turns aTask
into a synchronous process. This is a little bit likeTask.Result
but 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 aForkIO
and allow for launching parallel effects that you can await later or cancel.I see these as potential stop-gap functions for those moving from
v4
tov5
where they might find an*Async
variant 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.IAsyncEnumerable
LINQ 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