Strings and monads
Two small groups that end a pipeline in a type other than IEnumerable<T>.
JoinToString and ConcatToString turn a sequence into a string; they exist because string.Join and
string.Concat are static methods that read inside out at the end of a query.
Sequence and Traverse turn a sequence of monadic values into a monadic value of a sequence. They are how
the Option, Result and Either monads scale from one value to
many: every element must succeed, or the whole thing fails with the first failure.
JoinToString
string.Join as an extension method, so it can be the last step of a pipeline.
string JoinToString<TSource>(this IEnumerable<TSource> source, char separator)
string JoinToString<TSource>(this IEnumerable<TSource> source, string separator)
string JoinToString(this IEnumerable<string?> source, string separator)
Each element is converted with ToString, and the separator goes between the elements, never at the ends. An
empty sequence produces an empty string. The method is eager, as it must be.
string.Join(", ", items.Select(...)) reads inside out: the last operation comes first. items.Select(...).JoinToString(", ")
reads in the order things happen.
An empty separator is concatenation, and ConcatToString says so; λ1004
points this out.
Example
var names = Sequence.Return("Ada", "Grace", "Linus");
names.JoinToString(", ");
// "Ada, Grace, Linus"
Enumerable.Range(1, 5).JoinToString('-');
// "1-2-3-4-5"
var csvLine = record.Fields.Select(Escape).JoinToString(',');
ConcatToString
string.Concat as an extension method: all elements converted with ToString and glued together with nothing
in between.
string ConcatToString<TSource>(this IEnumerable<TSource> source)
The typical input is a sequence of char or of string, for example the result of a character-level
transformation that should become a string again.
Example
"hello".Reverse().ConcatToString();
// "olleh"
"Hello World".Where(char.IsLetter).ConcatToString();
// "HelloWorld"
var html = elements.Select(RenderElement).ConcatToString();
Sequence
Turns a sequence of monadic values inside out: an IEnumerable<Option<T>> becomes an Option<IReadOnlyList<T>>,
and likewise for Result<T>, Either<L, R>, Reader<E, T> and Lazy<T>.
Option<IReadOnlyList<TSource>> Sequence<TSource>(this IEnumerable<Option<TSource>> source)
Result<IReadOnlyList<TSource>> Sequence<TSource>(this IEnumerable<Result<TSource>> source)
Either<TLeft, IReadOnlyList<TSource>> Sequence<TLeft, TSource>(this IEnumerable<Either<TLeft, TSource>> source)
Reader<TEnvironment, IEnumerable<TSource>> Sequence<TEnvironment, TSource>(this IEnumerable<Reader<TEnvironment, TSource>> sequence)
Lazy<IEnumerable<TSource>> Sequence<TSource>(this IEnumerable<Lazy<TSource>> sequence)
For the three failure monads the semantics are "all or nothing": the result is Some, Ok or Right with
the list of all inner values if every element succeeded, and the first None, Error or Left otherwise.
The input is enumerated only up to that first failure, and the method is eager.
For Reader and Lazy there is no failure, only deferral: the result is a reader that, given an environment,
runs every inner reader, or a lazy value that evaluates every inner lazy. These two are lazy themselves.
Sequence differs from WhereSelect and
Partition in what a failure means. WhereSelect drops the failed
elements and keeps going. Partition keeps both the failures and the successes. Sequence says that one
failure fails the whole thing.
The name comes from Haskell, where sequence is the general operation of swapping two type constructors.
The inverse direction, swapping a monad with a sequence inside it, lives on the monad types themselves; see the
Result chapter.
Example
var allParsed = Sequence.Return("1", "2", "3").Select(ParseExtensions.ParseInt32OrNone).Sequence();
// Some([1, 2, 3])
var oneBad = Sequence.Return("1", "x", "3").Select(ParseExtensions.ParseInt32OrNone).Sequence();
// None
Select followed by Sequence is so common that it has its own name, Traverse.
Traverse
Select with a monadic selector, followed by Sequence: apply a function that may fail to every element, and
get either all the results or the first failure.
Option<IReadOnlyList<TItem>> Traverse<TSource, TItem>(this IEnumerable<TSource> source, Func<TSource, Option<TItem>> selector)
Result<IReadOnlyList<TValidResult>> Traverse<TSource, TValidResult>(this IEnumerable<TSource> source, Func<TSource, Result<TValidResult>> selector)
Either<TLeft, IReadOnlyList<TRight>> Traverse<TSource, TLeft, TRight>(this IEnumerable<TSource> source, Func<TSource, Either<TLeft, TRight>> selector)
Reader<TEnvironment, IEnumerable<TResult>> Traverse<TSource, TEnvironment, TResult>(this IEnumerable<TSource> source, Func<TSource, Reader<TEnvironment, TResult>> selector)
Lazy<IEnumerable<T>> Traverse<TSource, T>(this IEnumerable<TSource> source, Func<TSource, Lazy<T>> selector)
Everything said about Sequence applies: all or nothing for Option, Result and Either, with
enumeration stopping at the first failure; deferral for Reader and Lazy. Traverse just saves the
intermediate sequence of monads.
This is the operation to reach for when a validation or lookup must succeed for every element before anything is done with the results:
Example
Option<IReadOnlyList<int>> ParseAll(IEnumerable<string> input)
=> input.Traverse(ParseExtensions.ParseInt32OrNone);
ParseAll(Sequence.Return("1", "2", "3")); // Some([1, 2, 3])
ParseAll(Sequence.Return("1", "x", "3")); // None
Result<IReadOnlyList<Order>> PlaceAll(IEnumerable<OrderRequest> requests)
=> requests.Traverse(PlaceOrder); // the first Error wins; later requests are not attempted
Either<ValidationError, IReadOnlyList<Record>> ValidateAll(IEnumerable<string> lines)
=> lines.Traverse(ValidateLine);