Bot releases are hidden (Show)
Published by louthy 3 months ago
StateT
monad-transformer. One of the SelectMany
overloads wasn't propagating the state correctly.Prelude.local
that creates a local IO and resource environment renamed to localIO
to avoid conflicts with ReaderT.local
and Reader.local
liftIO
in Prelude
when
and unless
that take a K<M, bool>
as the source of the flag. Means any monad that binds a bool
can be used directly in when
and unless
, rather than having to lower it first.iff
- works like when
and unless
, but has an else
case. K<M, bool>
can be used directly also, meaning that if/then/else monadic expressions can be built without lowering.actions
to the Prelude
. Allows for chaining n
applicative actions, discarding their results, apart from the last one, which is returnedPublished by louthy 3 months ago
This is a minor release to fix: issue 1340.
Thanks to @HernanFAR for raising the issue with concise repro steps 👍
Published by louthy 4 months ago
I'm now moving the v5
release from alpha to beta. Not because I'm feature complete, but because from my real-world testing of v5
(with my new startup project) it is much more stable than I expected. In fact I haven't hit any issues at all outside of missing functionality.
So, this is more of a 'soft launch beta', primarily for those who were perhaps not ready to use language-ext in alpha form but are more likely to in beta form.
Published by louthy 5 months ago
I've re-added a Try<A>
monad (I always intended to re-add it, just hadn't got around to it). And I've and reimplemented the TryT<M, A>
monad-transformer in terms of Try
(K<M, Try<A>>
), previously it was implemented in terms of Fin
(Func<K<M, Fin<A>>>
).
I have also:
Match
and IfFail
methods for pattern matching on the success/fail state.|
operator @catch
overrides to allow for easy error handling.The IO
monad also has a .Try()
method that will run the IO
monad in a try/catch block returning IO<Fin<A>>
for more manual handling of IO errors.
Still needs some in-depth testing to make sure all exceptions are captured, but it's effectively feature complete.
Published by louthy 5 months ago
This release removes the ResourceT<M, A>
monad-transformer from language-ext and instead moves the functionality into the IO<A>
monad. ResourceT
required IO
to be in the transformer stack and so it really was adding complexity to a feature that's closely linked. This adds a tiny overhead to the IO
monad -- the IO monad already carried an environment through its computations, so this doesn't change much -- in the big scheme of things it's likely to bring performance benefits.
Some big improvements because of this:
use
and release
are now available in the Prelude
, which makes them easier to work with (no need for any manual generic arguments), everything is inferable from usage.IO
computation (launching it on a new thread) automatically creates a local resource environment for the fork and cleans it up when the forked operation is complete.IO
computation (repeat(computation)
) - will automatically clean up any resources acquired by use
in the computation (on each iteration).IO
computation (retry(computation)
) - will automatically clean up any resources (acquired with use
) when the computation fails, that mean retries don't accumulate resources unnecessarily.local(computation)
works as a 'super using
' -- in that it will automatically clean up any resources acquired with use
in the computation. This allows you to create local scopes where you just freely acquire resources and then have a clean-up happen automatically.
IO.local
for local cancellation contexts and Reader.local
for local environments. I'm also open to changing the names of the others. Ideally any name would be a single word so it's easy on the eye. So, nothing like localResource
or cleanUp
.bracket(Acq, Use, Err, Fin)
and bracket(Acq, Use, Fin)
- these are like try
\ catch
\ finally
blocks for more explicit resource usage:
Acq
- acquires the resourceUse
- uses the resourceErr
- is the catch blockFin
- is the finally blockAll the usual caveats apply: this is an alpha, this isn't fully tested, use at your own risk.
Published by louthy 7 months ago
WARNING: THIS IS AN ALPHA RELEASE AND SHOULD BE CONSUMED WITH CARE! NOT FOR PRODUCTION.
For those that don't know yet (and there's no reason to think you should, because I haven't announced it yet) -- the Pipes Effect
system now has the ability to lift any monad into its stack (previously it only allowed Aff
to be lifted). It is now a general monad transformer like ReaderT
, OptionT
, EitherT
, etc.
As, with all monad-transfomers, when you 'run' the transformer, it generates the lifted monad. You can think of this being like a mini-compiler that takes the monad stack and compiles down to the inner-most monad, which can then just be run as normal.
The problem for Pipes is that there's usually lots of recursion, repetition (using repeat
, retry
), or iteration (using yieldAll
, etc.). This is problematic when you don't know anything about the inner monad. The transformer can't run the inner monad, because it only has access to the Monad
interface (Bind
) and the inherited interfaces of Applicative
and Functor
(Apply
, Action
, Map
, and Pure
). So, doing iteration requires recursion, and recursion blows the stack in C#.
Previously Pipes were able to directly
Run
theAff
because the Pipe system knew it was working only withAff
. This allowed it to flatten the recursion.
Anyway, now Pipes has internal support for any Foldable
. That means yieldAll(...)
can take a sequence from any foldable (Range
, EnumerableM
, HashMap
, HashSet
, Lst
, Map
, Seq
, Either
, Option
, Validation
, Identity
, ... and any you write) and yield the values within the structure through the pipe. Functions like repeat(ma)
- which continually repeat an operation until it fails - have also been implemented internally as something that iterates over an infinite foldable.
This functionality has been enabled by adding a new method to the Applicative
trait: Actions
. You might know the existing Action(K<M, A> ma, K<M, B> mb)
method that runs the first applicative (ma
), ignores its result, and then runs the second applicative mb
, returning its result.
Actions
instead takes an IEnumerable<K<M, A>>
:
K<F, A> Actions<A>(IEnumerable<K<F, A>> fas)
It runs each applicative action and ignores its result, returning the result of the last item. That means a sequence of Proxy
values (Proxy
is the monad-transformer for pipes) can be mapped - the map will just run (using RunEffect
) the Proxy - producing a sequence of whatever the lifted inner-monad is for the Proxy
. This lazy sequence of monads can then be invoked by calling Actions
on it, which will lazily walk the sequence, evaluating the inner-monad one-by-one.
There is a default implementation, but it has the same lack of knowledge that Pipes had, so it should be overridden for computation based applicatives (that usually need invoking with without an argument). Here's the override for Eff<RT, A>
:
static K<Eff<RT>, A> Applicative<Eff<RT>>.Actions<A>(IEnumerable<K<Eff<RT>, A>> fas) =>
from s in getState<A>()
from r in Eff<RT, A>.Lift(
rt =>
{
Fin<A> rs = Errors.SequenceEmpty;
foreach (var kfa in fas)
{
var fa = kfa.As();
rs = fa.Run(rt, s.Resources, s.EnvIO);
if (rs.IsFail) return rs;
}
return rs;
})
select r;
You can see how:
And so, if you want to use your own monads with Pipes then you should implement Actions
.
There's still more to do with Pipes, but all of the examples in EffectsExamples
now work, which is a good sign!
WARNING: THIS IS AN ALPHA RELEASE AND SHOULD BE CONSUMED WITH CARE! NOT FOR PRODUCTION.
Published by louthy 7 months ago
WARNING: THIS IS AN ALPHA RELEASE AND SHOULD BE CONSUMED WITH CARE! NOT FOR PRODUCTION.
Free
monad doesn't need Alternative
trait: removedAppend
operator renamed to Combine
. 'Combine' works semantically for more of the monoidal associative operations than Append
(which really only makes sense with collections).SemigroupK
and MonoidK
-- these are like the Semigroup
and Monoid
traits except they work on K<M, A>
instead of A
. These are almost identical to SemiAlternative
and Alternative
execept they don't require the underlying value to an an Applicative
. The idea here is that SemigroupK
and MonoidK
would be used on types like collections that 'sum' when the Combine
operator is applied, whereas SemiAlternative
and Alternative
provide an alternative value when the Combine
operator is applied (coalescing).repeat
variants, retry
variants, and timeout
for the IO monadIO.yieldFor(TimeSpan)
. This is like Task.Delay
but for the IO monad. The war against async means that this does the thread-yielding internally, no need to call await. I figured yieldFor
is more meaningful than Delay
, it indicates that the thread is yielding, not simply blocking.ContT<R, M, A>
-- just the raw type for now.HeadOrNone
, HeadOrInvalid
, HeadOrLeft
, LastOrNone
, etc. have been removed.Head
and Last
are now Option
returning. This is a breaking change. Can be mitigated by either matching, casting, or invocation of .ValueUnsafe()
extension.Range
type -- previously there were several types (IntegerRange
, CharRange
, etc.) -- now there's just one: Range<A>
. It leverages the new traits built into .NET (IComparisonOperators
, IAddtionOperators
, etc.)Foldable.Sum
, Foldable.Max
, etc.: (IComparisonOperators
, IAddtionOperators
, IAdditiveIdentity
etc.) -- these are Microsoft's ugly named versions of monoids etc.I'm rapidly coming to the conclusion that extension-methods are a terrible idea. Especially in a library like language-ext where I am trying to present a consistent set of interfaces to types that share common traits. It's just impossible to enforce consistency and relies on the human eye -- and that errs regularly!
The latest move toward using traits is really starting to help reduce the extension methods, or at least mean the extension methods are hanging off traits rather than individual instance-types.
One change that I have made recently is to change Foldable
to require implementation of FoldWhile
and FoldWhileBack
instead of Fold
and FoldBack
. This means that so many more default behaviours can hang off of Foldable
-- and most of them are optimal. For example, Exists
-- which can stop processing as soon as its predicate returns true
-- couldn't early-out before.
And so, the foldable trait is now growing to have a ton of functionality. Also nested foldables!
However, quite a lot of those methods, like Sum
, Count
, etc. also exist on IEnumerable
. And so, for a type like Seq
which derives from both IEnumerable
and K<Seq, A>
, there will be extension method resolution issues.
So, the choice is to provide extension methods for IEnumerable
(an ill defined type) or for Foldable
- a super featureful type with the opportunity for implementers to provide bespoke optimised overrides.
Really, the choice should be easy: extensions for Foldable are just better than extensions for IEnumerable
. So, I have done that. The downside is that this will be another breaking change (because the IEnumerable
extensions have been removed). The fix is to convert from IEnumerable<A>
to EnumerableM<A>
using .AsEnumerableM()
. EnumerableM<A>
supports Foldable
(and other traits).
So, I've been working to remove as many non-trait extension methods as I can -- and I will continue to do so leading up to the beta. This will bring consistency to the code-base, reduce the amount of code, and provide ample opportunities for bespoke optimisations. Just be aware that this is another fix-up job.
WARNING: THIS IS AN ALPHA RELEASE AND SHOULD BE CONSUMED WITH CARE! NOT FOR PRODUCTION.
Published by louthy 8 months ago
This release should only be consumed by those who are interested in the new features coming in the monster v5
release.
Just to give you an idea of the scale of this change:
It is a monster and should be treated with caution...
If you add it to a production project, you should only do so to see (potentially) how many breaking changes there are. I would not advise migrating a production code-base until I get close to the final release.
I am also not going to go into huge detail about the changes here, I will simply list them as headings. I will do a full set of release notes for the beta
release. You can however follow the series of articles I am writing to help you all prep for v5
-- it goes (and will go) into much more detail about the features.
K<F, A>
- higher-kinds enabling interfaceFunctor.map
, Alternative.or
, StateM.get
, ...).Map
, .Or
, Bind
, etc.),BindT
, MapT
, etc. ), now fully generic.Option
, Either<L>
, etc.)Functor<F>
Applicative<F>
Monad<M>
Foldable<F>
Traversable<T>
Alternative<F>
SemiAlternative<F>
Has<M, TRAIT>
Reads<M, OUTER_STATE, INNER_STATE>
Mutates<M, OUTER_STATE, INNER_STATE>
ReaderM<M, Env>
StateM<M, S>
WriterM<M, OUT>
MonadT<M, N>
- Monad transformers
ReaderT<Env, M, A>
WriterT<Out, M, A>
StateT<S, M, A>
IdentityT<M, A>
EitherT<L, M, R>
ValidationT<F, M, S>
OptionT<M, A>
TryT<M, A>
IdentityT<M, A>
ResourceT<M, A>
Free<F, A>
- Free monadsIO<A>
- new IO monad that is the base for all IOEff<RT, A>
monad rewritten to use monad-transformers (StateT<RT, ResourceT<IO>, A>
)Eff<RT, A>
doesn't need HasCancel
trait (or any trait)Pure
/ Fail
monadsSeq1
made [Obsolete]
Semigroup<A>
and Monoid<A>
types have been refactoredTypeClass
class has been renamed Trait
Apply
extensions that use raw Func
removedSequence
extension methods have been removedTraverse
extension methods have been removedToComparer
doesn't exist on the Ord<A>
trait any moreLanguageExt.ClassInstances.Sum
Guard<E>
has become Guard<E, A>
UnitsOfMeasaure
namespace converted to a static classEither
doesn't support IEnumerable<EitherData>
any moreEither
'bi' functions have their arguments flippedTuple
and KeyValuePair
removedSome<A>
OptionNone
EitherUnsafe<L, R>
EitherLeft<L>
EitherRight<L>
Validation<MFail, Fail, A>
Try<A>
TryOption<A>
TryAsync<A>
TryOptionAsync<A>
Result<A>
OptionalResult<A>
Option<A>
ExceptionMatch
, ExceptionMatchAsync
, ExceptionMatchOptionalAsync
LanguageExt.SysX
LanguageExt.CodeGen
LanguageExt.Transformers
Published by louthy about 1 year ago
New:
IfFail
in Try
, TryOption
, TryAsync
, and TryOptionAsync
- Thanks @mark-pro 👍Effects
samplesBug fixes:
Published by louthy over 1 year ago
This is a fixes release.
OptionAsync
I have brought forward a change to OptionAsync
that I was saving for v5
: the removal of the async-awaiter. You can't now await
an OptionAsync
. The resulting value wasn't clear, and honestly the async
/await
machinery is really quite shonky outside of using it for Tasks.
I have made the OptionAsync
implementation aware of nullable references, and so you can now await
the Value
property instead:
public Task<A?> Value
That will reproduce the same behaviour as before. You can still await
the ToOption()
method, which returns a Task<Option<A>>
, if you want to do matching on the underlying option. Or call the various Match*
methods.
This release fixes the following issues:
Producer.merge
error handlingProducer merging was silently ignoring errors. They now exit and return the first error and shutdown other producers they were merged with. Merged producers also listen for cancellation correctly now.
Finally, you can only merge produces with a bound value of Unit
. This is to stop the silent dropping of their return value as well as the need to provide a final (bound) value for merged producers, which doesn't really make sense. That also means the +
operator can't work any more because it can't be defined for the Producer<..., A>
type. So you must use Producer.merge
.
This fixes an issue mentioned in: https://github.com/louthy/language-ext/issues/1177
repeatM
doesn't cause a stack-overflowCertain elements of the Pipes
capability of language-ext are direct ports from the Haskell Pipes library, which uses recursion everywhere. repeatM
was causing a stack-overflow on usage, this is now fixed.
Example usage:
public static Effect<Runtime, Unit> effect =>
Producer.repeatM(Time<Runtime>.nowUTC) | writeLine<DateTime>();
static Consumer<Runtime, X, Unit> writeLine<X>() =>
from x in awaiting<X>()
from _ in Console<Runtime>.writeLine($"{x}")
from r in writeLine<X>()
select r;
repeat
improvementsRemoved the Repeat
case from the Pipes DSL which simplifies it and brings it closer to the Haskell version. Updated the repeat
combinator function to use the same Enumerate
case that yieldAll
uses. This has benefits that it doesn't spread out when composed with other Proxy
types. This is should mean it's easier to pick bits of the expression to repeat, rather than the whole effect being repeated due to the spread.
Trampoline
Added trampolining functionality. It's relatively light at the moment, I am considering approaches to enable genuine recursion in the effects system. Don't rely on this, it may be removed if it doesn't prove useful and almost certainly will have API changes if it stays.
Published by louthy over 1 year ago
There Pipes functions: enumerate
, enumerate2
, observe
, observe2
have been deleted and replaced with yieldAll
(that accepts IEnumerable
, IAsyncEnumerable
, or IObservable
).
The previous implementation had mixed behaviours, some that always yielded the values, some that turned the remainder of the pipes expression into a enumeration. This wasn't entirely clear from the name and so now there is a single set of yieldAll
functions that always yield
all the values in the collection downstream.
The behaviour of the always yield enumerate
functions was also buggy, and didn't result in the remainder of a Producer
or Pipe
being invoked after the yield
. :
public static Effect<Runtime, Unit> effect =>
repeat(producer) | consumer;
static Producer<Runtime, int, Unit> producer =>
from _1 in Console<Runtime>.writeLine("before")
from _2 in yieldAll(Range(1, 5))
from _3 in Console<Runtime>.writeLine("after")
select unit;
static Consumer<Runtime, int, Unit> consumer =>
from i in awaiting<int>()
from _ in Console<Runtime>.writeLine(i.ToString())
select unit;
In the example above, "after"
would never be called, this is now fixed.
There is also a new &
operator overload for Pipes which performs the operations in series. This has the effect of concatenating Producers (for example), but will work for Pipe
, Consumer
, Client
, and Server
.
// yields [1..10]
static Producer<Runtime, int, Unit> producer =>
yieldAll(Range(1, 5)) & yieldAll(Range(6, 5));
There's still work to do on repeat
, but this was quite a difficult change, so I'll leave that for now.
Published by louthy almost 2 years ago
This release puts out the 4.3.*
beta changes:
And contains a number of contributed improvements:
And a number of contributed bug fixes:
Task.Cast
and ValueTask.Cast
Thanks to all those who contributed. I am still super busy with other projects right now, and I don't always get to PRs as quickly as I would like, but It's always appreciated.
Any problems, please report in the Issues.
Published by louthy over 2 years ago
There have been a number of calls on the Issues page for a ValidationAsync
monad, which although it's a reasonable request (and I'll get to it at some point I'm sure), when I look at the example requests, it seems mostly the requestors want a smarter error handling story in general (especially for the collection of multiple errors).
The error-type that I'm building most of the modern functionality around (in Fin
, Aff
, and Eff
for example) is the struct
type: Error
. It has been designed to handle both exceptional and expected errors. But the story around multiple errors was poor. Also, it wasn't possible to carry additional information with the Error
, it was a closed-type other than ability to wrap up an Exception
- so any additional data payloads was cumbersome and ugly.
Extending the
struct
type to be more featureful was asking for trouble, as it was already getting pretty messy.
Error
refactorSo, I've bitten the bullet and refactored Error
into an abstract record
type.
Error
sub-typesThere are a few built-in sub-types:
Exceptional
- An unexpected errorExpected
- An expected errorManyErrors
- Many errors (possibly zero)These are the key base-types that indicate the 'flavour' of the error. For example, a 'user not found' error isn't
something exceptional, it's something we expect to happen. An OutOfMemoryException
however, is
exceptional - it should never happen, and we should treat it as such.
Most of the time we want sensible handling of expected errors, and bail out completely for something exceptional. We also want to protect ourselves from information leakage. Leaking exceptional errors via public APIs is a sure-fire way to open up more information to hackers than you would like. The Error
derived types all try to protect against this kind of leakage without losing the context of the type of error thrown.
When Exceptional
is serialised, only the Message
and Code
component is serialised. There's no serialisation of the inner Exception
or its stack-trace. It is also possible to construct an Exceptional
message with an alternative message:
Error.New("There was a problem", exception);
That means if the Error
gets serialised, we only get a "There was a problem"
and an error-code.
Deserialisation obviously means we can't recover the
Exception
, but the state of theError
will still beExceptional
- so it's possible to carry the severity of the error across domain boundaries without leaking too much information.
Error
methods and propertiesEssentially an error is either created from an Exception
or it isn't. This allows for expected errors to be represented without throwing exceptions, but also it allows for more principled error handling. We can pattern-match on the
type, or use some of the built-in properties and methods to inspect the Error
:
IsExceptional
- true
for exceptional errors. For ManyErrors
this is true
if any of the errors are exceptional.IsExpected
- true
for non-exceptional/expected errors. For ManyErrors
this is true
if all of the errors are expected.Is<E>(E exception)
- true
if the Error
is exceptional and any of the the internal Exception
values are of type E
.Is(Error error)
- true
if the Error
matches the one provided. i.e. error.Is(Errors.TimedOut)
.IsEmpty
- true
if there are no errors in a ManyErrors
Count
- 1
for most errors, or n
for the number of errors in a ManyErrors
Head()
- To get the first errorTail()
- To get the tail of multiple errorsYou may wonder why
ManyErrors
could be empty. That allows forErrors.None
- which works a little likeOption.None
. We're saying: "The operation failed, but we have no information on why; it just did".
Error
constructionThe Error
type can be constructed as before, with the various overloaded Error.New(...)
calls.
For example, this is an expected error:
Error.New("This error was expected")
When expected errors are used with codes then equality and matching is done via the code only:
Error.New(404, "Page not found");
And this is an exceptional error:
try
{
}
catch(Exception e)
{
// This wraps up the exceptional error
return Error.New(e);
}
Finally, you can collect many errors:
Error.Many(Error.New("error one"), Error.New("error two"));
Or more simply:
Error.New("error one") + Error.New("error two")
Error
types with additional dataYou can extend the set of error types (perhaps for passing through extra data) by creating a new record that inherits Exceptional
or Expected
:
public record BespokeError(bool MyData) : Expected("Something bespoke", 100, None);
By default the properties of the new error-type won't be serialised. So, if you want to pass a payload over the wire, add the [property: DataMember]
attribute to each member:
public record BespokeError([property: DataMember] bool MyData) : Expected("Something bespoke", 100, None);
Using this technique it's trivial to create new error-types when additional data needs to be moved around, but also there's a ton of built-in functionality for the most common use-cases.
Error
breaking changesError
isn't a struct
any more, default(Error)
will now result in null
. In practice this shouldn't affect anyone.BottomException
is now in LanguageExt.Common
Error
documentationThere's also a big improvement on the API documentation for the Error
types
Aff
and Eff
applicative functorsNow that Error
can handle multiple errors, we can implement applicative behaviours for Aff
and Eff
. If you think of monads enforcing sequential operations (and therefore can only continue if each operation succeeds - leading to only one error report if it fails), then applicative-functors are the opposite in that they can run independently.
This is what's used for the
Validation
monads, to allow multiple operations to be evaluated, and then all of the errors collected.
By adding Apply
to Aff
and Eff
, we can now do the same kind of validation-logic both synchronously and asynchronously.
First let's create a simple asynchronous effect that delays for a period of time:
static Aff<Unit> delay(int milliseconds) =>
Aff(async () =>
{
await Task.Delay(milliseconds);
return unit;
});
Now we'll combine that so we get an effect that parses a string
into an int
, and adds a delay of 1000
milliseconds (the delay is to simulate calling some external IO).
:
static Aff<int> parse(string str) =>
from x in parseInt(str).ToAff(Error.New("parse error: expected int"))
from _ in delay(1000)
select x;
Notice how we're converting the
Option<int>
to anAff
, and providing an error value to use if theOption
isNone
Next we'll use the applicative behaviour of the Aff
to run two operations in parallel. When they complete the values will be applied to the function that has been lifted by SuccessAff
.
static Aff<int> add(string sx, string sy) =>
SuccessAff((int x, int y) => x + y)
.Apply(parse(sx), parse(sy));
To measure what we're doing, let's add a simple function called report
. All it does is run an Aff
, measures how long it takes, and prints the results to the screen:
static async Task report<A>(Aff<A> ma)
{
var sw = Stopwatch.StartNew();
var r = await ma.Run();
sw.Stop();
Console.WriteLine($"Result: {r} in {sw.ElapsedMilliseconds}ms");
}
Finally, we can run it:
await report(add("100", "200"));
await report(add("zzz", "yyy"));
The output for the two operations is this:
Result: Succ(300) in 1032ms
Result: Fail([parse error: expected int, parse error: expected int]) in 13ms
Notice how the first one (which succeeds) takes 1032ms
- i.e. the two parse operations ran in parallel. And on the second one, we get both of the errors returned. The reason that one finished so quickly is because the delay was after the parseInt
call, so we exited immediately.
Of course, it would be possible to do this:
from x in parse(sx)
from y in parse(sy)
select x + y;
Which is more elegant. But the success path would take 2000ms
, and the failure path would only report the first error.
Hopefully that gives some insight into the power of applicatives (even if they're a bit ugly in C#!)
This will be in beta for a little while, as the changes to the Error
type are not trivial.
Published by louthy over 2 years ago
The existing Schedule
type has been massively upgraded to support even more complex scheduling for repeating, retrying, and folding of Aff
and Eff
types.
A huge thanks to @bmazzarol who did all of the heavy lifting to make this feature a reality!
It has been refactored from the ground up, a Schedule
now is a (possibly infinite) stream of durations. Each duration indicates to the retry
, repeat
, and fold
behaviours how long to wait between each action. The generation of those streams comes from:
Schedule.Forever
- infinite stream of zero length durationsSchedule.Once
- one item stream of zero length durationSchedule.Never
- no durations (a schedule that never runs)Schedule.TimeSeries(1, 2, 3 ...)
- pass in your own durations to build a bespoke scheduleSchedule.spaced(space)
- infinite stream of space
length durationsSchedule.linear(seed, factor)
- schedule that recurs continuously using a linear back-offSchedule.exponential(seed, factor)
- schedule that recurs continuously using a exponential back-offSchedule.fibonacci(seed, factor)
- schedule that recurs continuously using a fibonacci based back-offSchedule.upto(max)
- schedule that runs for a given durationSchedule.fixedInterval(interval)
- if that action run between updates takes longer than the interval, then the action will run immediatelySchedule.windowed(interval)
- a schedule that divides the timeline into interval
-long windows, and sleeps until the nearest window boundary every time it recurs.Schedule.secondOfMinute(second)
- a schedule that recurs every specified second of each minuteSchedule.minuteOfHour(minute)
- a schedule that recurs every specified minute of each hourSchedule.hourOfDay(hour)
- a schedule that recurs every specified hour of each daySchedule.dayOfWeek(day)
- a schedule that recurs every specified day of each weekThese schedules are mostly infinite series, and so to control their 'length' we compose with ScheduleTransformer
values to create smaller series, or to manipulate the series in some way (jitter for example). The following functions generate ScheduleTransformer
values.
Schedule.recurs(n)
- Clamps the schedule durations to only recur n
times.Schedule.NoDelayOnFirst
- Regardless of any other settings, it makes the first duration zeroSchedule.RepeatForever
- Repeats any composed schedules foreverSchedule.maxDelay(max)
- limits the returned delays to max delay (upper clamping of durations).Schedule.maxCumulativeDelay(Duration max)
- keeps a tally of all the delays so-far, and ends the generation of the series once max
delay has passedSchedule.jitter(minRandom, maxRandom, seed)
- adds random jitter to the durationsSchedule.jitter(factor, seed)
- adds random jitter to the durationsSchedule.decorrelate(factor, seed)
- transforms the schedule by de-correlating each of the durations both up and down in a jittered way.Schedule.resetAfter(max)
- resets the schedule after a provided cumulative max durationSchedule.repeats(n)
- not to be confused with recurs, this repeats the schedule n
times.Schedule.intersperse(schedule)
- intersperse the provided schedule between each duration in the schedule.Schedule
and ScheduleTransformer
can be composed using |
(union) or &
(intersection):
var schedule = Schedule.linear(1 * sec) | Schedule.recurs(3) | Schedule.repeat(3);
// [1s, 2s, 3s, 1s, 2s, 3s, 1s, 2s, 3s]
Union |
will take the minimum of the two schedules to the length of the longest, intersect &
will take the maximum of the two schedules to the length of the shortest.
One thing remaining to-do is to bring
HasTime<RT>
back into theCore
and allow these schedules to use injectable time. Some of the functions already take aFunc<DateTime>
to access 'now', this will be expanded so time can be sped up or slowed down, with the schedules 'just working'. That'll be in the next few weeks I'm sure, and is related to this issue.
Check out the API documentation to see what's what. And again, thanks to @bmazzarol for the hard work 👍
Published by louthy over 2 years ago
The transformers extensions, which are a big set of T4 templates for generating extension methods for nested monadic types have now been broken out into their own package: LanguageExt.Transformers
If you use the following functions: BindT
, MapT
, FoldT
, FoldBackT
, ExistsT
, ForAllT
, IterT
, FilterT
, PlusT
, SubtractT
, ProductT
, DivideT
, SumT
, CountT
, AppendT
, CompareT
, EqualsT
, or ApplyT
- then you will get compile errors, and will need to add a reference to the LanguageExt.Transformers
package.
I've done this for a couple of reasons:
LanguageExt.Core
library. This change takes the Core
package from 3,276 kb
to 2,051 kb
.
Core
library will always be quite chunky because of the sheer amount of features, but the transformer extension methods definitely aren't always needed, so breaking them out made senseThe main transformer extensions that remain in the Core
library are:
Traverse
Sequence
These are so heavily used that I believe moving them out into the Transformers
library would mean everyone would be obliged to use it, and therefore it wouldn't achieve anything. There may be an argument for bringing BindT
and MapT
back into the core at some point. I will see how this plays out (it wouldn't be a future breaking change if that were the case).
Any problems, please report via the Issues in the usual way.
Published by louthy over 2 years ago
Language-ext had been in beta for a few months now. Today we go back to full 'RTM' mode.
The recent changes are:
AtomHashMap
and Ref
change-trackingA major new feature that allows for tracking of changes in an AtomHashMap
or a Ref
(using STM). The Change
event publishes HashMapPatch<K, V>
which will allow access to the state of the map before and after the change, as well as a HashMap<K, Change<V>>
which describes the transformations that took place in any transactional event.
In the LanguageExt.Rx
project there's various observable stream extensions that leverage the Change
event:
OnChange()
- which simply streams the HashMapPatch<K, V>
OnEntryChange()
- which streams the Change<V>
for any key within the mapOnMapChange()
- which streams the latest HashMap<K, V>
snapshotRef
which represents a single value in the STM system, has a simpler Change
event that simply streams the latest value. It also has an Rx
extension, called Change()
.
Documented in previous beta release notes
TrackingHashMap<K, V>
This is a new immutable data-structure which is mostly a clone of HashMap
; but one that allows for changes to be tracked. This is completely standalone, and not related to the AtomHashMap in any way other than it's used by the AtomHashMap.Swap
method. And so, this has use-cases of its own.
Changes are tracked as a HashMap<K, Change<V>>
. That means there's at most one change-value stored per-key. So, there's no risk of an ever expanding log of changes, because there is no log! The changes that are tracked are from the 'last snapshot point'. Which is from the empty-map or from the last state where tracking HashMap.Snapshot()
is called.
Documented in previous beta release notes
Aff
and Eff
systemThe Aff
and Eff
system had some unfortunate edge-cases due to the use of memoisation by-default. The underlying system has been simplified to be more of a standard reader-monad without memoisation. You can still memoise if needed by calling: ma.Memo()
.
Future changes will make the
Aff
andEff
into more of a DSL, which will allow for certain elements of the system to be 'pure', and therefore safely memoisable, and other elements not. My prototype of this is in too early a stage to release though, so I've taken the safer option here.
Breaking change: Both Clone
and ReRun
have been removed, as they are now meaningless.
LanguageExt.SysX
- for .NET5.0+ featuresThe LanguageExt.Sys
package is a wrapper for .NET BCL IO functionality, for use with the runtime Eff
and Aff
monads. This is going to stay as netstandard2.0
for support of previous versions of the .NET Framework and .NET Core. This new package adds features that are for net5.0+
.
The first feature to be supported is the Activity
type for Open-Telemetry support.
ScheduleAff
and ScheduleEff
usage was inconsistentDepending on the monad they were used with, you might see a 'repeat' that was 1 greater than it should have been. This is now fixed.
Seq
equality fixUnder certain rare circumstances it was possible for the equality operator to error with a lazy Seq
. Fix from @StefanBertels - thanks!
It seams c# isn't smart enough to turn a new A[0]
into a non-allocating operation. And so they have all been replaced with Array.Empty()
. Fix from @timmi-on-rails - thanks!
Any problems, please report them in the Issues as usual. Paul 👍
Published by louthy over 2 years ago
As requested by @CK-LinoPro in this Issue. AtomHashMap
and Ref
now have Change
events.
Change<A>
Change<A>
is a new union-type that represents change to a value, and is used by AtomHashMap
and TrackingHashMap
.
You can pattern-match on the Change
value to find out what happened:
public string WhatHappened(Change<A> change) =>
change switch
{
EntryRemoved<A> (var oldValue) => $"Value removed: {oldValue}",
EntryAdded<A> (var value) => $"Value added: {value}",
EntryMapped<A, A>(var from, var to) => $"Value mapped from: {from}, to: {to}",
_ => "No change"
};
EntryMapped<A, B>
is derived from EntryMappedFrom<A>
and EntryMappedTo<B>
, so for any A -> B
mapping change, you can just match on the destination value:
public string WhatHappened(Change<A> change) =>
change switch
{
EntryRemoved<A> (var oldValue) => $"Value removed: {oldValue}",
EntryAdded<A> (var value) => $"Value added: {value}",
EntryMappedTo<A>(var to) => $"Value mapped from: something, to: {to}",
_ => "No change"
};
That avoids jumping through type-level hoops to see any changes!
There are also various 'helper' properties and methods for working with the derived types:
Member | Description |
---|---|
HasNoChange |
true if the derived-type is a NoChange<A>
|
HasChanged |
true if the derived-type is one-of EntryRemoved<A> or EntryAdded<A> or EntryMapped<_, A>
|
HasAdded |
true if the derived-type is a EntryAdded<A>
|
HasRemoved |
true if the derived-type is a EntryRemoved<A>
|
HasMapped |
true if the derived-type is a EntryMapped<A, A>
|
HasMappedFrom<FROM>() |
true if the derived-type is a EntryMappedFrom<FROM>
|
ToOption() |
Gives the latest value from the Change , as long as the Change is one-of EntryAdded or EntryMapped or EntryMappedTo
|
There are also constructor functions to build your own Change
values.
AtomHashMap<K, V>
and AtomHashMap<EqK, K, V>
The two variants of AtomHashMap
both now have Change
events that can be subscribed to. They emit a HashMapPatch<K, V>
value, which contains three fields:
Field | Description |
---|---|
From |
HashMap<K, V> that is the state before the change |
To |
HashMap<K, V> that is the state after the change |
Changes |
HashMap<K, Change<V>> that describes the changes to each key |
There are three related Rx.NET
extensions in the LanguageExt.Rx
package:
AtomHashMap Extension |
Description |
---|---|
OnChange() |
Observable stream of HashMapPatch<K, V>
|
OnMapChange() |
Observable stream of HashMap<K, V> , which represents the latest snapshot of the AtomHashMap
|
OnEntryChange() |
Observable stream of (K, Change<V>) , which represents the change to any key within the AtomHashMap
|
var xs = AtomHashMap<string, int>();
xs.OnEntryChange().Subscribe(pair => Console.WriteLine(pair));
xs.Add("Hello", 456);
xs.SetItem("Hello", 123);
xs.Remove("Hello");
xs.Remove("Hello");
Running the code above yields:
(Hello, +456)
(Hello, 456 -> 123)
(Hello, -123)
Swap
method (potential) breaking-changeThe implementation of Swap
has changed. It now expects a Func<TrackingHashMap<K, V>, TrackingHashMap<K, V>>
delegate instead of a Func<HashMap<K, V>, HashMap<K, V>>
delegate. This is so the Swap
method can keep track of arbitrary changes during the invocation of the delegate, and then emit them as events after successfully committing the result.
Ref<A>
Refs are used with the atomic(() => ...)
, snapshot(() => ...)
, and serial(() => ...)
STM transactions. Their changes are now tracked during a transaction, and are then (if the transaction is successful) emitted on the Ref<A>.Change
event. These simply publish the latest value.
As before there's Rx
extensions for this:
Ref Extension |
Description |
---|---|
OnChange() |
provides an observable stream of values |
var rx = Ref("Hello");
var ry = Ref("World");
Observable.Merge(rx.OnChange(),
ry.OnChange())
.Subscribe(v => Console.WriteLine(v));
atomic(() =>
{
swap(rx, x => $"1. {x}");
swap(ry, y => $"2. {y}");
});
This outputs:
1. Hello
2. World
TrackingHashMap
This is a new immutable data-structure which is mostly a clone of HashMap
; but one that allows for changes to be tracked. This is completely standalone, and not related to the AtomHashMap
in any way other than it's used by the AtomHashMap.Swap
method. And so, this has use-cases of its own.
Changes are tracked as a HashMap<K, Change<V>>
. That means there's at most one change-value stored per-key. So, there's no risk of an ever expanding log of changes, because there is no log! The changes that are tracked are from the 'last snapshot point'. Which is from the empty-map or from the last state where trackingHashMap.Snapshot()
is called.
var thm = TrackingHashMap<int, string>();
Console.WriteLine(thm.Changes);
thm = thm.Add(100, "Hello");
thm = thm.SetItem(100, "World");
Console.WriteLine(thm.Changes);
This will output:
[]
[(100: +World)]
Note the +
, this indicates an Change.EntryAdded
. And so there has been a single 'add' of the key-value pair (100, World)
. The "Hello"
value is ignored, because from the point of the snapshot: a value has been added. That's all we care about.
If I take a snapshot halfway through, you can see how this changes the output:
var thm = TrackingHashMap<int, string>();
thm = thm.Add(100, "World");
thm = thm.Snapshot();
thm = thm.SetItem(100, "Hello");
Console.WriteLine(thm.Changes);
This outputs:
[(100: World -> Hello)]
So the snapshot is from when there was a (100, World)
pair in the map.
Hopefully that gives an indication of how this works!
Published by louthy almost 3 years ago
@StefanBertels has rightly highlighted some possible edge-cases with yesterday's rollout. These edge cases could turn into very real bugs in your application, and so I am:
Seq1
: Seq1
will continue to be the way you create singleton Seq<A>
collections.Seq
conversion functions to toSeq
. This will be consistently be a compilation error, which you can fix up by just renaming the Seq(blah)
to toSeq(blah)
or blah.ToSeq()
.It isn't ideal that Seq1
is still hanging around, being all inconsistent with the rest of the library, but neither is putting a burden of tracking all possible edge cases. And so, this is the right thing to do.
What I will do is pause the migration of Seq1
to Seq
until June 2022. That should give everyone enough time to get to version 4 and migrate their Seq
conversion function usage to toSeq
. Then the subsequent change will have zero impact.
For those that don't upgrade in that time and want the safest possible migration, they can upgrade to a pre-June 2022 version, fix the conversion functions and then upgrade to the latest.
Apologies to anyone who started the migration yesterday.
Published by louthy almost 3 years ago
This is the backstory: When language-ext was still in version 1, there was no Seq<A>
collection type. There were however Seq(...)
functions in the Prelude
. Those functions coerced various types into being IEnumerable
, and in the process protected against any of them being null
.
These are they:
Seq <A> (A? value)
Seq <A> (IEnumerable<A> value)
Seq <A> (A[] value)
Seq <A> (Arr<A> value)
Seq <A> (IList<A> value)
Seq <A> (ICollection<A> value)
Seq <L, R> (Either<L, R> value)
Seq <L, R> (EitherUnsafe<L, R> value)
Seq <A> (Try<A> value)
Seq <A> (TryOption<A> value)
Seq <T> (TryAsync<T> value)
Seq <T> (TryOptionAsync<T> value)
Seq <A> (Tuple<A> tup)
Seq <A> (Tuple<A, A> tup)
Seq <A> (Tuple<A, A, A> tup)
Seq <A> (Tuple<A, A, A, A> tup)
Seq <A> (Tuple<A, A, A, A, A> tup)
Seq <A> (Tuple<A, A, A, A, A, A> tup)
Seq <A> (Tuple<A, A, A, A, A, A, A> tup)
Seq <A> (ValueTuple<A> tup)
Seq <A> (ValueTuple<A, A> tup)
Seq <A> (ValueTuple<A, A, A> tup)
Seq <A> (ValueTuple<A, A, A, A> tup)
Seq <A> (ValueTuple<A, A, A, A, A> tup)
Seq <A> (ValueTuple<A, A, A, A, A, A> tup)
Seq <A> (ValueTuple<A, A, A, A, A, A, A> tup)
When Seq<A>
the type was added, I needed constructor functions, which are these:
Seq <A> ()
Seq1 <A> (A value)
Seq <A> (A a, A b)
Seq <A> (A a, A b, A c, params A[] ds)
The singleton constructor needed to be called Seq1
because it was clashing with the original single-argument Seq
functions from v1 of language-ext.
This has been bothering me for a long time. So, it's time to take the hit.
All of the legacy coercing functions (the first list) are now renamed to toSeq
. And Seq1
has been renamed to Seq
(well, Seq1
still exists, but it's marked [Obsolete]
.
The breaking change is that if you use any of those legacy Seq
functions, you'll either need to change, this:
Seq(blah);
Into,
toSeq(blah);
Or,
blah.ToSeq();
There were quite a lot of the coercion functions used in language-ext, and it took me about 10 minutes to fix it up. The good thing is that they all turn into compilation errors, no hidden behaviour changes.
Apologies for any inconvenience with this. I figure that as we've switched into v4 that this would be a good time for a clean break, and to tidy up something that I have actively seen cause confusion for devs. Seq
is the most performance immutable list in the .NET sphere, so it shouldn't really be confusing to use!
Published by louthy about 3 years ago
Here it is, version 4.0
of language-ext - we're finally out of beta. I had hoped to get some more documentation done before releasing, but I have ever decreasing free time these days, and I didn't want to delay the release any further. A lot has happened since going into beta about a year or so ago:
How to handle side-effects with pure functional code has been a the perennial question. It is always coming up in the issues and it's time for this library to be opinionated on that. Language-ext already has Try
and TryAsync
which are partial solutions to this problem, but they only really talk to the exception catching part of IO. Not, "How do we inject mocked behaviours?", "How do we deal with configuration?", "How do we describe the effects that are allowed?", "How do we make async and sync code play well together?" etc.
Language-Ext 4.0 has added four new monadic types:
Eff<A>
- for synchronous side-effects. This is a natural replacement for Try<A>
Aff<A>
- for asynchronous side-effects. This is a natural replacement for TryAsync<A>
Eff<RT, A>
- for synchronous side-effects with an injectable runtime. This is like the Reader<Env, A>
monad on steroids!Aff<RT, A>
- for asynchronous side-effects with an injectable runtime. This is like the ReaderAsync<Env, A>
monad that never existed!These four monads have been inspired by the more bespoke monads I'd been building personally to deal with IO in my private projects, as well as the Aff
and Eff
monads from PureScript, and also a nod to ZIO from the Scala eco-system. Together they form the Effects System of language-ext, and will be the key area of investment going forward.
These four monads all play well together, they can be used in the same LINQ expression and everything just works.
There is now a whole section of the wiki dedicated to dealing with side-effects from first principles. I ran out of steam to complete it, but I hope to get back to it soon. It does go into depth on the Aff
and Eff
monads, and how to build runtimes.
On top of that, there's a project called EffectsExamples in the Samples folder. It is a whole application, with an effects driven menu system, for showing what's possible.
Finally, there's a new library called LanguageExt.Sys
that wraps up the core side-effect producing methods in .NET and turns them into Aff
or Eff
monads. So far it covers:
This library will grow over time to cover more of .NET. You are not limited to using these: the runtime system is fully extensible, and allows you to add your own side-effecting behaviors.
Pipes is a C# interpretation of the amazing Haskell Pipes library by Gabriella Gonzalez. It is a streaming library that has one type: Proxy
. This one type can represent:
Each Proxy variant can be composed with the others, and when you've got it correct they all fuse into an Effect
that can be run. For example, a Producer composed with a Pipe, and then composed with a Consumer will fuse into a runnable effect. Each component is standalone, which means it's possible to build reusable effects 'components'
It also supports either uni or bi-directional streaming and can work seamlessly with the effects-system.
It's actually been in language-ext for a long time, but it was borderline unusable because of the amount of generic arguments that were needed to make it work. Since then I've augmented it with additional monads and 100s of SelectMany
overrides that make it easier to use. I have also removed all recursion from it, which was a problem before.
The original Haskell version is a monad transformer, meaning that it can wrap other monads. Usually, you'd wrap up the IO monad, but it wasn't limited to that. However, the C# version can't be as general as that, so it only wraps the Aff
and Eff
monads. However, I have managed to augment the original to also support:
IEnumerable
IObservable
IAsyncEnumerable
This means we can handle asynchronous and synchronous streams. All of which play nice with the effects system. Here's a small example:
public class TextFileChunkStreamExample<RT> where RT :
struct,
HasCancel<RT>,
HasConsole<RT>,
HasFile<RT>,
HasTextRead<RT>
{
public static Aff<RT, Unit> main =>
from _ in Console<RT>.writeLine("Please type in a path to a text file and press enter")
from p in Console<RT>.readLine
from e in mainEffect(p)
select unit;
static Effect<RT, Unit> mainEffect(string path) =>
File<RT>.openRead(path)
| Stream<RT>.read(80)
| decodeUtf8
| writeLine;
static Pipe<RT, SeqLoan<byte>, string, Unit> decodeUtf8 =>
from c in awaiting<SeqLoan<byte>>()
from _ in yield(Encoding.UTF8.GetString(c.ToReadOnlySpan()))
select unit;
static Consumer<RT, string, Unit> writeLine =>
from l in awaiting<string>()
from _ in Console<RT>.writeLine(l)
select unit;
}
mainEffect
is where the Effect
is fused by composing the File.openRead
producer, the Stream.read
pipe, the decodeUtf8
pipe, and the writeLine
consumer.SeqLoan
. This is a version of Seq
that has a borrowed backing array from an ArrayPool
. The Pipes system automatically cleans up resources for you. And so, this stream doesn't thrash the GCNOTE: The client/server aspect of this still has the generics problem, and is difficult to use. I will be doing the same work I did for Producers, Pipes, and Consumers to make that more elegant over time.
Although this library is all about immutability and pure functions, it is impossible (at least in C#) to get away from needing to mutate something. Usually it's a shared cache of some sort. And that's what the concurrency part of language-ext is all about, dealing with that messy stuff. We already had Atom
for lock-free atomic mutation of values and Ref
for use with the STM system.
In this release there have been some major improvements to Atom
and Ref
(no more deadlocks). As well as the addition of three new lock-free atomic collections:
AtomHashMap
AtomSeq
VersionHashMap
And two other data-structures:
VersionVector
VectorClock
All but VectorClock
, VersionVector
, and VersionHashMap
were detailed in previous release notes, so I won't cover those again. VectorClock
, VersionVector
, and VersionHashMap
allow you to easily work with distributed versioned changes. Vector-clocks comes from the genius work of Leslie Lamport amongst others, and allow for tracking of causal or conflicting events; when combined with some data in VersionVector
it's possible to resolve many clients wanting to change the same piece of data, knowing who did what first. When VersionVector
is combined with an atomic-hash-map, you get VersionHashMap
. This manages multiple keys of VersionVectors, making it a short hop to a distributed database.
There's some good blog posts on the Riak site that explain vector clocks.
when
and unless
These two functions make working with alternative values easier in LINQ expressions.
from txt1 in File<RT>.readAllText(path)
from txt2 in when(txt1 == "", SuccessEff("There wasn't any text in the file, so use this instead!"))
select txt;
The dual of when
is unless
, it simply reverses the predicate.
guard
and guardnot
These are similar to when
and unless
in that they take a predicate, but instead they return the monad's alternative value (Left
for Either
, Error
for Fin
, and Aff
, for example). This makes it easy to catch problems and generate contextual error messages.
Another Haskell library I've taken inspiration from is Pretty. It's synopsis is:
Pretty is a pretty-printing library, a set of API's that provides a way to easily print out text in a consistent format of your choosing. This is useful for compilers and related tools.
It is based on the pretty-printer outlined in the paper 'The Design of a Pretty-printing Library' by John Hughes in Advanced Functional Programming, 1995
It's a fully composable text document building system that can understand layouts and can do lovely reformatting functions depending on context (like the desired width of text, the tabs, etc.). If you've ever had to manage printing of text with indented tabs, this is the feature for you.
It's pretty new in the library, I needed it for some compiler work I was working on, and it seemed generally useful - so I've added it. However, there's no documentation other than what's in the code. It is a 1-to-1 clone of the original Haskell library, and so if you can understand their docs, you should be good.
Traverse
and Sequence
were originally code-gen'd using T4 templates. Some pairings of monads didn't play well in this scenario. And so I bit the bullet and decided to write them all by hand. This was particularly onerous. Luckily I had some help from @skobka , @blakeSaucier , @harrhp, and @iamim - they kindly helped build a massive suite of unit-tests to prove that the 100s of functions I'd written by hand weren't wrong!
For pairings where there's asynchrony involved, there is also now TraverseSerial
, TraverseParallel
, SequenceSerial
, and SequenceParallel
. These can be tuned to use smaller or larger windows of tasks, to limit the amount of parallelism, if required.
These release notes are already huge. A lot has happened, lots of bugs fixed, lots of performance improvements ...
These are the best of the rest: