Functional programming is the oldest of the three major programming paradigms, none the less it is the last which gets wide spread usage. Even in languages like C++, Java or C# we want to use a functional style of programming.
Linq is the first Monad which got wide spread use in C#, and most C# programmers were not even aware of it beeing a monad, which probably helped.
Mark Seemann points out that "Unfortunately, Maybe implementations often come with an API that enables you to ask a Maybe object if it's populated or empty, and a way to extract the value from the Maybe container. This misleads many programmers [...]"
This library is based on his example code, and should grow slowly to a library which helps to use and understand the Functional programming paradigm. Functional programming is side-effect free and the strong type system can be used to make illegal state impossible.
Use functional programming as an additional asset to write correct code.
Migrating from 2.x to 3.0
- Update to the latest 2.x version of Funcky (2.7.1) and fix all warnings.
- Update to Funcky 3.0.
- Run
dotnet format analyzers
. This will migrateOption<T>.None()
toOption<T>.None
for you.
This command sometimes fails while loading your project(s).--no-restore
might help with that. - Build and fix other compilation failures.
- You might need to re-run
dotnet format analyzers
after fixing other errors. - Important: Check if you're using
System.Text.Json
to serializeOption
s. This will no longer work automatically. You need to add theOptionJsonConverter
yourself.
Functional Programming
- No side effects
- Pure functions
- Referential transparency
- Follow the types
- Higher order functions
- Composition
Simplify if null
by using an Option
We start off with the following code: (Note that some types have been omitted for brevity)
#nullable enable
using System;
public class Example
{
public VersionEnvironment? GetCurrentVersionEnvironment(PackageName packageName)
{
var currentVersion = ReadCurrentVersion(packageName);
if (currentVersion is null)
{
return null;
}
var versionPath = GetVersionPath(packageName, currentVersion);
return new VersionEnvironment(currentVersion, versionPath);
}
public PackageVersion? ReadCurrentVersion(PackageName name) => null; // Real implementation omitted
public string GetVersionPath(PackageName name, PackageVersion version) => null!; // Real implementation omitted
}
The function GetCurrentVersionEnvironment
doesn't do much, but it's not pleasant to look at,
because of that early return condition.
Step Ⅰ
We start off by changing the return type of ReadCurrentVersion
from PackageVersion?
to an Option
:
public Option<PackageVersion> ReadCurrentVersion(PackageName name) => Option.None<PackageVersion>(); // Real implementation omitted
Step Ⅱ
This immediately breaks the GetCurrentVersionEnvironment
, because our null
check no longer makes sense.
Instead of checking for null
and returning early, we can use Select
on the Option
to project its value.
public Option<VersionEnvironment> GetCurrentVersionEnvironment(PackageName packageName)
{
return ReadCurrentVersion(packageName)
.Select(currentVersion => {
var versionPath = GetVersionPath(packageName, currentVersion);
return new VersionEnvironment(currentVersion, versionPath);
});
}
Step Ⅲ
This is already much simpler, since we've got rid of that explicit null check.
There's still room for simplification, since our projection takes up two lines and we ideally only want a single expression in our projection.
We can achieve this by translating our expression to query syntax:
public Option<VersionEnvironment> GetCurrentVersionEnvironment(PackageName packageName)
{
return from currentVersion in ReadCurrentVersion(packageName)
let versionPath = GetVersionPath(packageName, currentVersion)
select new VersionEnvironment(currentVersion, versionPath);
}
Step Ⅳ
Since our method now consists of only one expression, we can make it an expression body:
public Option<VersionEnvironment> GetCurrentVersionEnvironment(PackageName packageName)
=> return from currentVersion in ReadCurrentVersion(packageName)
let versionPath = GetVersionPath(packageName, currentVersion)
select new VersionEnvironment(currentVersion, versionPath);
Formatting a calendar page
This case study is taken from Component programming with ranges by By H. S. Teoh written for D.
We shall use as example the classic task of laying out a yearly calendar on the console, such that given a particular year, the program will print out a number of lines that displays the 12 months in a nice grid layout, with numbers indicating each day within the month. Something like this:
January February March
1 2 1 2 3 4 5 6 1 2 3 4 5
3 4 5 6 7 8 9 7 8 9 10 11 12 13 6 7 8 9 10 11 12
10 11 12 13 14 15 16 14 15 16 17 18 19 20 13 14 15 16 17 18 19
17 18 19 20 21 22 23 21 22 23 24 25 26 27 20 21 22 23 24 25 26
24 25 26 27 28 29 30 28 29 27 28 29 30 31
31
April May June
1 2 1 2 3 4 5 6 7 1 2 3 4
3 4 5 6 7 8 9 8 9 10 11 12 13 14 5 6 7 8 9 10 11
10 11 12 13 14 15 16 15 16 17 18 19 20 21 12 13 14 15 16 17 18
17 18 19 20 21 22 23 22 23 24 25 26 27 28 19 20 21 22 23 24 25
24 25 26 27 28 29 30 29 30 31 26 27 28 29 30
July August September
1 2 1 2 3 4 5 6 1 2 3
3 4 5 6 7 8 9 7 8 9 10 11 12 13 4 5 6 7 8 9 10
10 11 12 13 14 15 16 14 15 16 17 18 19 20 11 12 13 14 15 16 17
17 18 19 20 21 22 23 21 22 23 24 25 26 27 18 19 20 21 22 23 24
24 25 26 27 28 29 30 28 29 30 31 25 26 27 28 29 30
31
October November December
1 1 2 3 4 5 1 2 3
2 3 4 5 6 7 8 6 7 8 9 10 11 12 4 5 6 7 8 9 10
9 10 11 12 13 14 15 13 14 15 16 17 18 19 11 12 13 14 15 16 17
16 17 18 19 20 21 22 20 21 22 23 24 25 26 18 19 20 21 22 23 24
23 24 25 26 27 28 29 27 28 29 30 25 26 27 28 29 30 31
30 31
While intuitively straightforward, this task has many points of complexity.
Although generating all dates in a year is trivial, the order in which they must be processed is far from obvious. Since we're writing to the console, we're limited to outputting one line at a time; we can't draw one cell of the grid and then go back up a few lines, move a few columns over, and draw the next cell in the grid. We have to somehow print the first lines of all cells in the top row, followed by the second lines, then the third lines, etc., and repeat this process for each row in the grid. Of course, we could create an internal screen buffer that we can write to in arbitrary order, and then output this buffer line-by-line at the end, but this approach is not as elegant because it requires a much bigger memory footprint than is really necessary.
In any case, as a result of this mismatch between the structure of the calendar and the structure of the output, the order in which the days in a month must be processed is not the natural, chronological order. We have to assemble the dates for the first weeks in each of the first 3 months, say, if we are putting 3 months per row in the grid, print those out, then assemble the dates for the second weeks in each month, print those out, etc.. Furthermore, within the rows representing each week, some days may be missing, depending on where the boundaries of adjacent months fall; these missing days must be filled out in the following month's first week before the first full week in the month is printed. It is not that simple to figure out where a week starts and ends, and how many rows are needed per month. Then if some months have more full weeks than others, they may occupy less vertical space than other months on the same row in the grid; so we need to insert blank spaces into these shorter months in order for the grid cells to line up vertically in the output.
With this level of complexity, writing our calendar program using the traditional ad hoc way of resolving structure conflicts will certainly result in very complex, hard-to-understand, and bug-prone code. There would not be much hope of getting any reusable pieces out of it.
Nonetheless the end result will look pretty simple, and it will be completley streamable.
Create the date-range
There are obviously many ways to achieve this, you could write your own Implementation of IEnumerable<T>
with a hand written IEnumerator<T>
or you could simply write a function and take advantage of yield
to create the iterator.
We want to avoid to write such a function and take advantage of the numerous generators availaible in Funcky.
Sequence.Successors
creates an infinite sequence of days, starting with January first of the given year. We take all the values in the same year, so this sequence should yield all the days of a year.
return Sequence.Successors(JanuaryFirst(year), NextDay)
.TakeWhile(IsSameYear(year));
Most of these helper function are straight forward, but IsSameYear
might be a bit special if you havent worked with curried functions before.
The function IsSameYear
takes one parameter and returns a function which takes another parameter, this is also called the curried form of a function, there is also the Functional.Curry
and Functional.Uncurry
functions which can transform between both forms without the need to write them both. IsSameYear(2000)
returns a function which always returns true
if the Date is from the year 2000. That way of using functions might come in handy a lot more often than you think.
private static DateOnly NextDay(DateOnly day)
=> day.AddDays(1);
private static DateOnly JanuaryFirst(int year)
=> new(year: year, month: 1, day: 1);
private static Func<DateOnly, bool> IsSameYear(int year)
=> day
=> day.Year == year;
This makes the the main body of the code semantic very easy to understand. All the helper functions are trivially to understand on it's own too.
Group this into months
LINQ offers you the GroupBy function which we could use easily in this case. This way we would have 12 groups, in each group we would have all the days of one month of the given year.
return Sequence.Successors(JanuaryFirst(year), NextDay)
.TakeWhile(IsSameYear(year))
.GroupBy(d => d.Month)
There are two things we should consider here though.
1.) GroupBy is like the SQL GROUP BY and can rearrange elements, and therefore is not a lazy Extension function. 2.) GroupBy would also Group all days from a different year into the same 12 montly buckets.
Often that is exactly what you want, but in this case if we think of an endless stream of days this is not what we need at all. The days do not need to be rearranged, all days in the same month are next to each other and if we find a second January, we would like to have a new 13th bucket.
That is why Funcky offers the extension function AdjacentGroupBy
. It is a fully lazy, forward only GroupBy
. Exactly what we need here.
return Sequence.Successors(JanuaryFirst(year), NextDay)
.TakeWhile(IsSameYear(year))
.AdjacentGroupBy(d => d.Month);
Now we have an IEnumerable<IEnumerable<DateOnly>>
, where the inner IEnumerable
is representing a single month.
Layout a single month
Since we have now all the days of the same month, we want to create a layout for a single month out of these days.
Currently we have a sequence of months. To transform the inner element, from a sequence of days into something else, we need to make a projection which is done with Select
in C#.
return Sequence.Successors(JanuaryFirst(year), NextDay)
.TakeWhile(IsSameYear(year))
.AdjacentGroupBy(d => d.Month);
.Select(LayoutMonth)
The LayoutMonth
function therefore gets a sequence of all the day days in a single month, and should produce a Layout for that single Month.
This is actually pretty straight forward. What do we want? We want to format a single month, and how would a single month look like? So the result will be a sequence of strings, each string representing one line of the layout.
April
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
First, we decompose the problem once more: we start with one line where we print the month, then a few lines which represent a week and we also have an empty line at the end, but that is just layout sugar.
We need a few helper functions again: the most important step is grouping the days by week, and then we pass each week to a FormatWeek function.
private static IEnumerable<string> LayoutMonth(IEnumerable<DateOnly> month)
{
yield return CenteredMonthName(month);
foreach (var week in month.AdjacentGroupBy(GetWeekOfYear).Select(FormatWeek))
{
yield return week;
}
yield return $"{string.Empty,WidthOfAWeek}";
}
This is a fine way to do this, it is very easy to read, but the foreach is really annoying. We already have an IEnumerable<T>
, we just want to add it between the first and the last line. But with yield you are often limited to very procedural constructs.
Often you can avoid that, for simple cases we have Sequence.Return, Concat and other helpers, in this case though the nicest way is probaly creating an ImmutableList, because the syntax allows to combine ranges and single items elegantly.
private static IEnumerable<string> LayoutMonth(IEnumerable<DateOnly> month)
=> ImmutableList<string>.Empty
.Add(CenteredMonthName(month))
.AddRange(FormatWeeks(month))
.Add(new string(' ', WidthOfAWeek));
Let's dive into our helper functions. First we take a look at the name of the month. The only noteworthy detail is the very functional mindest seen in the solution to the centering problem. It uses a pattern match to fill in the missing spaces: it is not very efficent, but easy to understand. The recursion will be very short because our lines are only 21 characters wide.
private static string CenteredMonthName(IEnumerable<DateOnly> month)
=> month
.First()
.ToString(MonthNameFormat)
.Center(WidthOfAWeek);
internal static class StringExtensions
{
public static string Center(this string text, int width)
=> (width - text.Length) switch
{
0 => text,
1 => $" {text}",
_ => Center($" {text} ", width),
};
}
We have already seen the heart of FormatWeeks
in the yield solution, but now it is a separate function. FormatWeeks
again needs 2 very simple helper functions, the first one projects the week of the year, the other one will format a sequence of days.
The sequence of days can be either a complete week, or a partial week from the beginning or the end of the month. But because of the way we construct these sequences, there always is at least one element in it.
private static IEnumerable<string> FormatWeeks(IEnumerable<DateOnly> month)
=> month
.AdjacentGroupBy(GetWeekOfYear)
.Select(FormatWeek);
The GetWeekOfYear function is just calling the API with the correct parameters and always using the current culture and a little fiddling with DateTime (because DateOnly was introduced far too late in C# 10).
private static int GetWeekOfYear(DateOnly dateTime)
=> CultureInfo
.CurrentCulture
.Calendar
.GetWeekOfYear(dateTime.ToDateTime(default), CultureInfo.CurrentCulture.DateTimeFormat.CalendarWeekRule, CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek);
We are almost done with FormatMonth
, now we really format the week, each day has a width of 3 characters, we see that in FormatDay
. This means each line should have the same width of 21 characters. We simply pad the week correctly with the PadWeek
function.
private static string FormatWeek(IGrouping<int, DateOnly> week)
=> PadWeek(week.Select(FormatDay).ConcatToString(), week);
private static string FormatDay(DateOnly day)
=> $"{day.Day,WidthOfDay}";
We can ignore the full weeks, because they are already 21 characters long. How do we distinguish the beginning of the month from the end? The week at the end of the month must start with the first day of the week. So we pad accordingly from the left or the right.
private static string PadWeek(string formattedWeek, IGrouping<int, DateOnly> week)
=> StartsOnFirstDayOfWeek(week)
? $"{formattedWeek,-WidthOfAWeek}"
: $"{formattedWeek,WidthOfAWeek}";
Now it just boils down to, what is the start of the week? It might be a surprise, that this is actually the most difficult part of this program, because the DayOfWeek
enum defines Sunday as the first day of the week, which is not true for the largest part of the world, including all of europe. But CultureInfo
has our back, because it tells us in the DateTimeFormat
, which day of the week is the first day. However even that needs some calculation with % DaysInAWeek
.
The function NthDayOfWeek
gives us a 0 based index beginning with the start of the week. So we can simply check with is FirstDayOfTheWeek
that we are indeed on the first day of the week. And this works independent of the given culture. Sweet.
private static bool StartsOnFirstDayOfWeek(IGrouping<int, DateOnly> week)
=> NthDayOfWeek(week.First().DayOfWeek) is FirstDayOfTheWeek;
private static int NthDayOfWeek(DayOfWeek dayOfWeek)
=> (dayOfWeek + DaysInAWeek - CultureInfo.CurrentCulture.DateTimeFormat.FirstDayOfWeek) % DaysInAWeek;
Now we have an IEnumerable<IEnumerable<string>>
, where the inner one is still representing a single month, but each line is already a completeley formatted week.
Layouting the months together.
At this point we could print a list of months, we just would need to join all the lines together and it would look like this. (shortend to 2 months)
January
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
February
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29
The next step looks simple: we would need to take line 1, 8, 15 and create the first line of the final output:
January February March
And then lines 2, 9 and 16 would be combined to:
1 2 3 4 5 1 2 1
To do this lazily we use Chunk
.
Chunk
is supported with .NET 6, before that Funcky has nearly identical Replacement. (The return type is slightly different)
Chunk is grouping a sequnce into multiple sequnces of the same length. In our case we want to make the sequence of 12 months in to 4 sequences of length 3.
const MonthsPerRow = 3;
private static string CreateCalendarString(int year)
=> Sequence.Successors(JanuaryFirst(year), NextDay)
.TakeWhile(IsSameYear(year))
.AdjacentGroupBy(day => day.Month)
.Select(LayoutMonth)
.Chunk(MonthsPerRow);
That means we have now an IEnumerable<IEnumerable<IEnumerable<string>>>
where in the outermost IEnumerable
we group 3 months together respectivly.
Why does that help us with the layout? To create the first line of our final layout, we need the first lines of each month, and then the second, and so on. So we need to group the months together.
One of these chunks now looks like this:
Januar | 1 2 3 4 5 | 6 7 8 9 10 11 12 | 13 14 15 16 17 18 19 | 20 21 22 23 24 25 26 | 27 28 29 30 31 |
Februar | 1 2 | 3 4 5 6 7 8 9 | 10 11 12 13 14 15 16 | 17 18 19 20 21 22 23 | 24 25 26 27 28 29 |
März | 1 | 2 3 4 5 6 7 8 | 9 10 11 12 13 14 15 | 16 17 18 19 20 21 22 | 23 24 25 26 27 28 29 | 30 31 |
That actually already looks a lot like what we want, but we want the months on the top not on the left.
It also looks like a matrix, and on a matrix the operation to flip the rows and the columns (on a diagonal symmetry) is called transpose.
Funcky has a lazy Transpose
extension function for IEnumerable<IEnumerable<T>>
as long as it is a regular matrix (same length for each subsequence).
private static string CreateCalendarString(int year)
=> Sequence.Successors(JanuaryFirst(year), NextDay)
.TakeWhile(IsSameYear(year))
.AdjacentGroupBy(day => day.Month)
.Select(LayoutMonth)
.Chunk(MonthsPerRow)
.Select(chunk => chunk.Transpose())
After this transforamtion our chunk of 3 months looks like this:
Januar | Februar | März
1 2 3 4 5 | 1 2 | 1
6 7 8 9 10 11 12 | 3 4 5 6 7 8 9 | 2 3 4 5 6 7 8
13 14 15 16 17 18 19 | 10 11 12 13 14 15 16 | 9 10 11 12 13 14 15
20 21 22 23 24 25 26 | 17 18 19 20 21 22 23 | 16 17 18 19 20 21 22
27 28 29 30 31 | 24 25 26 27 28 29 | 23 24 25 26 27 28 29
| | 30 31
I think it is obvious that at this point, we are done. We just have to join the chunks together to have our final output.
private static string CreateCalendarString(int year)
=> Sequence.Successors(JanuaryFirst(year), NextDay)
.TakeWhile(IsSameYear(year))
.AdjacentGroupBy(day => day.Month)
.Select(LayoutMonth)
.Chunk(MonthsPerRow)
.Select(EnumerableExtensions.Transpose)
.Select(JoinLine)
.SelectMany(Identity)
.JoinToString(Environment.NewLine);
For a working solution with all the details, you should take a look at the complete code in Program.cs
Program.cs
The complete calendar program:
using System.Collections.Immutable;
using Funcky.Extensions;
using Funcky;
using Funcky.Monads;
using static System.Globalization.CultureInfo;
using static Funcky.Functional;
namespace Calendar;
internal static class StringExtensions
{
public static string Center(this string text, int width)
=> (width - text.Length) switch
{
0 => text,
1 => $" {text}",
_ => Center($" {text} ", width),
};
}
internal class Program
{
private const int FirstDayOfTheWeek = 0;
private const int MonthsPerRow = 3;
private const int DaysInAWeek = 7;
private const int WidthOfDay = 3;
private const int WidthOfAWeek = WidthOfDay * DaysInAWeek;
private const string MonthNameFormat = "MMMM";
private static void Main(string[] args)
=> Console.WriteLine(CreateCalendarString(ExtractYear(args).GetOrElse(DateTime.Now.Year)));
private static Option<int> ExtractYear(IEnumerable<string> args)
=> args.FirstOrNone()
.AndThen(ParseExtensions.ParseInt32OrNone);
private static string CreateCalendarString(int year)
=> Sequence.Successors(JanuaryFirst(year), NextDay)
.TakeWhile(IsSameYear(year))
.AdjacentGroupBy(day => day.Month)
.Select(LayoutMonth)
.Chunk(MonthsPerRow)
.Select(EnumerableExtensions.Transpose)
.Select(JoinLine)
.SelectMany(Identity)
.JoinToString(Environment.NewLine);
private static DateOnly NextDay(DateOnly day)
=> day.AddDays(1);
private static DateOnly JanuaryFirst(int year)
=> new(year: year, month: 1, day: 1);
private static Func<DateOnly, bool> IsSameYear(int year)
=> day
=> day.Year == year;
private static IEnumerable<string> JoinLine(IEnumerable<IEnumerable<string>> sequence)
=> sequence.Select(t => t.JoinToString(" "));
private static IEnumerable<string> LayoutMonth(IEnumerable<DateOnly> month)
=> ImmutableList<string>.Empty
.Add(CenteredMonthName(month))
.AddRange(FormatWeeks(month))
.Add(new string(' ', WidthOfAWeek));
private static IEnumerable<string> FormatWeeks(IEnumerable<DateOnly> month)
=> month
.AdjacentGroupBy(GetWeekOfYear)
.Select(FormatWeek);
private static string FormatWeek(IGrouping<int, DateOnly> week)
=> PadWeek(week.Select(FormatDay).ConcatToString(), week);
private static string FormatDay(DateOnly day)
=> $"{day.Day,WidthOfDay}";
private static string PadWeek(string formattedWeek, IEnumerable<DateOnly> week)
=> StartsOnFirstDayOfWeek(week)
? $"{formattedWeek,-WidthOfAWeek}"
: $"{formattedWeek,WidthOfAWeek}";
private static bool StartsOnFirstDayOfWeek(IEnumerable<DateOnly> week)
=> NthDayOfWeek(week.First().DayOfWeek) is FirstDayOfTheWeek;
private static int NthDayOfWeek(DayOfWeek dayOfWeek)
=> (dayOfWeek + DaysInAWeek - CurrentCulture.DateTimeFormat.FirstDayOfWeek) % DaysInAWeek;
private static int GetWeekOfYear(DateOnly dateTime)
=> CurrentCulture
.Calendar
.GetWeekOfYear(
dateTime.ToDateTime(default),
CurrentCulture.DateTimeFormat.CalendarWeekRule,
CurrentCulture.DateTimeFormat.FirstDayOfWeek);
private static string CenteredMonthName(IEnumerable<DateOnly> month)
=> month
.First()
.ToString(MonthNameFormat)
.Center(WidthOfAWeek);
}
Option Monad
What is the Option Monad
The Option Monad is a very simple algebraic type which is a fancy way to say you can have more than one different type of data in it. The Option monad is a combination of a value of a type and a second type which can only be one value: None
. The state of the option monad is always either None, or Some with a certain value of a type you can chose. It means it can hold any value of your chosen type + None state.
This is very similar to references which can be null
or Nullable Value Types which adds the "has no value"-concept to value types.
However the main issue with references and Nullable<T>
is, before every access you need to check if the value is accessible. The Option Monad is an abstraction which removes all this boilerplate code, in a save way.
Create something
var something = Option.Some(1337);
Create nothing
var nothing = Option<int>.None();
Select
Option<bool> maybeBool =
from m in maybe
select m == 1337;
Select Many
var result = from number in someNumber
from date in someDate
select Tuple.Create(number, date);
Match
bool isSome = maybe.Match(
none: false,
some: m => true
);
How can I get the value?
If you declare
int? integer = 1337;
You can access the Value directly via i.Value
. The typical beginner question on the monad is therefore how to get to the value in a monad.
The Option-Monad intentionally has no way to get to the value directly because that would be an unsafe operation. The whole point of an optional is that it sometimes has no value. Instead you should inject the behaviour into the monad.
The basic Example:
int? integer = MaybeValue();
if (integer.HasValue())
{
Console.WriteLine($"Value: {integer.Value}");
}
Injecting the behaviour:
Option<int> integer = MaybeValue();
integer
.AndThen(i => Console.WriteLine($"Value: {i}"));
Or in Linq syntax:
from integer in MaybeValue()
select Console.WriteLine($"Value: {integer.Value}");
The TryVerb-pattern
The TryVerb pattern is used in several instances in C# as an alternative for functions which throw an exception.
Parse
throws an exception if inputString
is not a number.
var number = int.Parse(inputString);
TryParse
returns false
in such a case, so the number is always correct if TryParse
returns true
. This means you have to check the return value before accessing number.
if (int.TryParse(inputString, out number))
{
// ...
}
Out parameters are bad, and in consequence we think the TryVerb-pattern (TryGet
, TryParse
...) used in C# as an anti-pattern.
We have added an overload for each and every "Try" function we have found in the .NET Framework and we give an alternative in the Form
GetValueOrNone
Extension functions have been added to IDictionary
and IReadOnlyDictionary
GetValuesOrNone
The parse functions
Option<int> = "1234".ParseIntOrNone();
The static class Funcky.Functional
is designed to be used with a static import (using static Funcky.Functional;
). All examples will be as if using static
was used.
All the methods in Funcky.Functional
are named to be easily understood without the functional prefix. They are general purpose, and their goal is to unify typical boilerplate code or be more expressive than typical C# ways of doing their job.
The NoOperation
function is a more expressive way of manually creating an empty statement as a parameter to a method expecting a Action
/Action<T>
, supporting from 0 up to 8 generic parameters.
Example 1:
// The function we want to call:
public void DoSomething(int value, Action<int> callback)
{
// ...
}
// How we would usually call it when we don't need the callback:
DoSomething(2, _ => {});
// How you can call it with NoOperation:
DoSomething(2, NoOperation);
NoOperation
becomes especially useful when a Action<T>
with many parameters is expected.
Example 2:
// The function we want to call:
public void DoSomething(int value, Action<int, string, float, AnyCustomClassThatYouWant> callback)
{
// ...
}
// How we would usually call it when we don't need the callback (C#9):
DoSomething(2, (_, _, _, _) => {});
// Before C#9, this was even worse:
DoSomething(3, (_, __, ___, ____) => {});
// How you can call it with NoOperation:
DoSomething(2, NoOperation);
NoOperation
is also useful when you want to use a expression body for a method.
Example 3:
// Abstract class:
public abstract class SomeClassWithExecuteAndHookBase
{
public void Execute();
protected abstract void PostExecutionHook();
}
// Derived class that doesn't have any use for the PostExecutionHook usually:
public class SomeClassWithExecuteAndHookDerived : SomeClassWithExecuteAndHookBase
{
protected override void PostExecutionHook()
{
}
}
// Derived class that doesn't have any use for the PostExecutionHook with NoOperation:
public class SomeClassWithExecuteAndHookDerived : SomeClassWithExecuteAndHookBase
{
protected override void PostExecutionHook() => NoOperation();
}
The Identity
function is designed to replace parameter-returning lambdas, like sometimes used in LINQ.
Example 1:
// Method:
public void FunctionExpectingSelector<TIn, TOut>(Func<TIn, TOut> selector)
{
// ...
}
// Usually:
FunctionExpectingSelector(x => x);
// Or:
FunctionExpectingSelector(item => item);
// With Identity:
FunctionExpectingSelector(Identity);
Example 2 (typical SelectMany
selector):
// Usually result of a query:
IEnumerable<IEnumerable<int>> itemGroups = new[] { new[] { 1, 2, 3 }, new[] { 5, 6, 7 } };
// Goal: Get all items flattened.
// Common approach:
itemGroups.SelectMany(x => x);
// Or:
itemGroups.SelectMany(items => items);
// With Identity:
itemGroups.SelectMany(Identity);
The unit type is an alternative to the native C# struct Void
(with the global alias void
).
The C# compiler handles Void
/void
very different to all other types in C#:
Void
can never be used. You must always usevoid
void
can not be instantiatedvoid
can not be used as a generic argument to any function- A function (sometimes also called method) "returning"
void
does not return a value, and the result of the function (void
) can not be assigned to any variable - When using reflection, void-methods will return
null
because the compiler can not know during compile-time what the dynamically dispatched method will return
These limitations on the void type introduce annoying behaviour, especially with expressions and generics.
Example 1 - the "void is not a real type" dilemma:
This examples uses a simple algebraic datatype with a generic match method:
public abstract class SimpleAlgebraicDatatype
{
private SimpleAlgebraicDatatype()
{
}
public abstract TResult Match<TResult>(
Func<Variant1, TResult> variant1,
Func<Variant2, TResult> variant2);
public class Variant1 : SimpleAlgebraicDatatype
{
public Variant1(string someValue)
{
SomeValue = someValue;
}
public string SomeValue { get; }
public override TResult Match<TResult>(
Func<Variant1, TResult> variant1,
Func<Variant2, TResult> variant2)
=> variant1(this);
}
public class Variant2 : SimpleAlgebraicDatatype
{
public Variant2(int someValue)
{
SomeValue = someValue;
}
public int SomeValue { get; }
public override TResult Match<TResult>(
Func<Variant1, TResult> variant1,
Func<Variant2, TResult> variant2)
=> variant2(this);
}
}
It then can be used like that:
SimpleAlgebraicDatatype variant = new SimpleAlgebraicDatatype.Variant1("Hey");
// get the variant as string
var value = variant.Match(
variant1: variant1 => variant1.SomeValue,
variant2: variant2 => Convert.ToString(variant2.SomeValue));
Console.WriteLine(value);
But if you don't want to use the return value, you're stuck with returning a value you don't want. This does not compile:
// Error [CS0411] The type arguments for method 'method' cannot be inferred from the usage. Try specifying the type arguments explicitly.
variant.Match(
variant1: variant1 => Console.Write(variant1.SomeValue),
variant2: variant2 => Console.Write(Convert.ToString(variant2.SomeValue)));
Now you have to decide what it returns. One option is to return null - the best fitting return type is probably object?
in that case:
variant.Match<object?>(
variant1: variant1 =>
{
Console.Write(variant1.SomeValue);
return null;
},
variant2: variant2 =>
{
Console.Write(Convert.ToString(variant2.SomeValue));
return null;
});
This is very unstatisfying however. We have to trick the type-system. There should be a more expressive way.
Funcky.Unit to the rescue:
variant.Match(
variant1: variant1 =>
{
Console.Write(variant1.SomeValue);
return Unit.Value;
},
variant2: variant2 =>
{
Console.Write(Convert.ToString(variant2.SomeValue));
return Unit.Value;
});
Now this isn't really less noise. This is why we created ActionToUnit.
This clears up the code to:
variant.Match(
variant1: variant1 => ActionToUnit(() => Console.Write(variant1.SomeValue)),
variant2: variant2 => ActionToUnit(() => Console.Write(Convert.ToString(variant2.SomeValue))));
See ActionToUnit for an explanation.
Example 2 - the "switch expression must return something" dilemma:
The following two code snippes do not comple:
// Error [CS0201]: Only assignment, call, increment, decrement, and new object expressions can be used as a statement.
variant switch
{
SimpleAlgebraicDatatype.Variant1 variant1 => Console.Write(variant1.SomeValue),
SimpleAlgebraicDatatype.Variant2 variant2 => Console.Write(Convert.ToString(variant2.SomeValue)),
_ => throw new Exception("Unreachable"),
};
// Error [CS0029]: Cannot implicitly convert type 'thorw-expression' to 'void'
// Error [CS9209]: A value of type 'void' may not be assigned.
_ = variant switch
{
SimpleAlgebraicDatatype.Variant1 variant1 => Console.Write(variant1.SomeValue),
SimpleAlgebraicDatatype.Variant2 variant2 => Console.Write(Convert.ToString(variant2.SomeValue)),
_ => throw new Exception("Unreachable"),
};
One way to resolve this dilemma is to return a method that returns null from every arm, and execute it after:
// very verbose and not very readable
Func<SimpleAlgebraicDatatype, object> action = variant switch
{
SimpleAlgebraicDatatype.Variant1 variant1 => _ =>
{
Console.Write(Convert.ToString(variant1.SomeValue));
return null;
},
SimpleAlgebraicDatatype.Variant2 variant2 => _ =>
{
Console.Write(Convert.ToString(variant2.SomeValue));
return null;
},
_ => _ => throw new Exception("Unreachable"),
};
action(variant);
If we use ActionToUnit once again, we can simplify this code, by a lot:
_ = variant switch
{
SimpleAlgebraicDatatype.Variant1 variant1 => ActionToUnit(() => Console.Write(variant1.SomeValue)),
SimpleAlgebraicDatatype.Variant2 variant2 => ActionToUnit(() => Console.Write(Convert.ToString(variant2.SomeValue))),
};
If you cannot use the discard syntax (_ =
), simply use var _
or similar, and ignore the variable after.
The ActionToUnit
function wraps a action into a function that returns a Funcky.Unit
instance.
You could write this method for yourself like this:
public static Func<Unit> ActionToUnit(Action action) => (Func<Unit>) (() =>
{
action();
return new Unit(); // or default, or Funcky.Unit.Value
});
However, if you now wanted a wrapper for a method with a parameter going into the action, you would need to write this:
public static Func<T, Unit> ActionToUnit<T>(Action<T> action) => (Func<T, Unit>) (parameter =>
{
action(parameter);
return new Unit();
});
Now with 2 parameters, you would need 2 generic parameters, and so on.
We already support everything from 0 up to 8 in the static class Functional
,
so you can just write using static Funcky.Functional
in your using section, and start using ActionToUnit
.
For some use cases, see the Unit Type documentation.
Here one example with a switch expression:
var value = GetValue();
_ = value switch
{
"Known" => ActionToUnit(() => Console.Write("Known")),
_ => ActionToUnit(() => Console.Write("Unknown")),
};
Extensions-methods on IEnumerable
LINQ offers you many important higher order functions to transform IEnumerables in many ways.
Funcky offers a few additional ones which can come in handy.
AdjacentGroupBy
AverageOrNone
CartesianProduct
In mathematics, specifically set theory, the Cartesian product of two sets A and B, denoted A×B, is the set of all ordered pairs (a, b) where a ∈ A and b ∈ B.
In other words: The Cartesian product produces all possible pairs of two given IEnumerable
s.
Recipe
The Cartesian product can be easily implemented ad-hoc using LINQ's built-in SelectMany
extension function:
using System;
using System.Linq;
// Version A: Get each pair as a tuple
var result = sequenceA.SelectMany(_ => sequenceB, ValueTuple.Create);
// Version B: Transform each pair using a selector
var result = sequenceA.SelectMany(_ => sequenceB, (a, b) => ...);
// Version C: Using LINQs declarative query syntax
var result =
from a in sequenceA
from b in sequenceB
select ...;
Examples
Two sequences as input:
smiles = [😀, 😐, 🙄]
fruits = [🍉, 🍌, 🍇, 🍓]
The Cartesian products of smiles and fruits:
smiles × fruits => [[😀, 🍉], [😀, 🍌], [😀, 🍇], [😀, 🍓],
[😐, 🍉], [😐, 🍌], [😐, 🍇], [😐, 🍓],
[🙄, 🍉], [🙄, 🍌], [🙄, 🍇], [🙄, 🍓]]
In this C# example you see how all playing cards are in fact a Cartesian products of a suit and a value.
This example uses the overload with a selector, because we just want a sequence of strings.
using System;
using System.Linq;
using Funcky;
var suits = Sequence.Return("♠", "♣", "♥", "♦");
var values = Sequence.Return("2", "3", "4", "5", "6", "7", "8", "9", "T", "J", "Q", "K", "A");
var deck = suits.SelectMany(_ => values, (suit, value) => $"{value}{suit}");
Chunk
With the .Chunk(int)
extension method, you can turn an IEnumerable<T>
into a IEnumerable<IEnumerable<T>>
, with the inner Enumerables being of the given size.
Empty and negative chunk sizes are not allowed and will throw a ArgumentOutOfRangeException
.
Examples
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
var chunked = numbers.Chunk(3);
// Result: IEnumerable with Chunks of size 3:
// 1st Chunk: 1, 2, 3
// 2nd Chunk: 4, 5, 6
// 3rd Chunk: 7, 8, 9
When the last chunk isn't complete, we get a smaller, incomplete last chunk:
var numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7 };
var chunked = numbers.Chunk(4);
// Result: IEnumerable with Chunks of size 4:
// 1st Chunk: 1, 2, 3, 4
// 2nd Chunk: 5, 6, 7
If required, you can also pass a result selector, that turns the inner IEnumerables into a different type:
var magicSquare = new List<int> { 4, 9, 2, 3, 5, 7, 8, 1, 6 };
var result = magicSquare.Chunk(3, Enumerable.Average); // equivalent to magicSquare.Chunk(3, number => Enumerable.Average(number));
// Result: IEnumerable<int> with 5, 5, 5 as items
ForEach
With the .ForEach
extension method, you can invoke an action for each item in an enumerable, just like a foreach
statement would allow you to do.
This method is already available in .NET, but just on List
s, and it makes sense for it to be available on every enumerable.
Keep in mind that .ForEach
is imperative and only expects an Action<T>
. It should not be used to change state of anything outside of the .ForEach
.
If you want to combine the enumerable into a result, consider using .Aggregate()
, as that is designed for such use-cases.
Example
// Original
foreach (var item in Items)
{
DoSomething(item);
}
// Using `.ForEach`
Items.ForEach(DoSomething); // equivalent to Items.ForEach(item => DoSomething(item));
FirstOrNone
Inspect
With the .Inspect
extension method, you can invoke an action for each item in an enumerable,
just like .ForEach
or the foreach
statement would allow you to do, but the method yields the initial enumerable back.
This can be useful when you want to apply a side-effect to a list before returning, or continue selecting on a list after applying a side-effect.
Inspect
can be especially useful when you want to log step(s) of a complex query, since you don't have to change the structure of the code to use it.
Examples
// Original using .ForEach
var items = someList.Select(TransformSomething);
Items.ForEach(DoSomething);
return items;
// Using `.Inspect`
return someList.Select(TransformSomething).Inspect(DoSomething);
// Original using foreach
var items = someList.Select(TransformToSomething);
foreach (var item in items)
{
DoSomething(item);
}
var transformedItems = items.Select(TransformToSomethingElse);
// Using `.Inspect`
var transformedItems = someList
.Select(TransformSomething)
.Inspect(DoSomething)
.Select(TransformToSomethingElse);
Deferred Execution
It is important to understand at which moment .Inspect
is executed. The exact moment of execution is the same as if it were a Select
, Where
or any other deferred LINQ-method.
See Microsoft Docs for more information about deferred execution in LINQ.
This is also an important difference between .ForEach
(eager) and .Inspect
(deferred).
Consider the following example:
Enumerable.Range(1, 100)
.Inspect(n => Console.WriteLine($"before where: {n}"))
.Where(n => n % 2 == 0)
.Inspect(n => Console.WriteLine($"after where: {n}"))
.Inspect(Console.WriteLine)
.Take(2)
.ToImmutableList(); // <- Side effects of .Inspect happen here
// Prints:
// before where: 1
// before where: 2
// after where: 2
// 2
// before where: 3
// before where: 4
// after where: 4
// 4
Interleave
Intersperse
Materialize
Merge
Examples
Given two sequences which are already ordered the same way:
sequence1 = [1, 2, 7, 9, 14]
sequence2 = [3, 6, 8, 11, 12, 16]
By merging we get one single sequence with the all elements of the given sequences with the same order.
sequence1.Merge(sequence2) =>
[1, 2, 7, 9, 14 ]
[ 3, 6, 8, 11, 12, 16]
-------------------------------------
[1, 2, 3, 6, 7, 8, 9, 11, 12, 14, 16]
None
With the .None
extension method, you can make !enumerable.Any()
calls easier.
That's all there is. You can replace:
if (!enumerable.Any()) { ... }
with the easier to read
if (enumerable.None()) { ... }
Just like with .Any()
, you can additionally pass a predicate as a parameter:
if (enumerable.None(item => item.SomeNumericProperty == 2) { ... }
PairWise
Example
animals = [ 🐵, 🐶, 🐺, 🐱, 🦄, 🐷, 🦁]
animals.PairWise() =>
[[🐵, 🐶],
[🐶, 🐺],
[🐺, 🐱],
[🐱, 🦄],
[🦄, 🐷],
[🐷, 🦁]]
Partition
Example
plantBasedFood = [🍉, 🍩 , 🎂, 🍌, 🍫, 🍓, 🍒, 🥕, 🌽, 🥧 ]
plantBasedFood.Partition(IsProcessedFood?)
=> [[🍩 , 🎂, 🍫, 🥧],
[🍉, 🍌, 🍓, 🍒, 🥕, 🌽]]
PowerSet
Shuffle
SlidingWindow
Split
TakeEvery
Transpose
WhereNotNull
WhereSelect
WithFirst
WithIndex
WithLast
WithPrevious
Example
animals = [ 🦄, 🐺, 🐷, 🦁, 🐵, 🐶 ]
animals.WithPrevious() =>
[[∅, 🦄],
[🦄, 🐺],
[🐺, 🐷],
[🐷, 🦁],
[🦁, 🐵],
[🐵, 🐶]]
ZipLongest
String Extensions
IndexOf
The classical IndexOf
methods provide a special form of error handling by returning -1
when nothing is found.
This is very cumbersome and a potential footgun, since you're not forced to check the return value.
Funcky offers extension methods on string
for each overload of IndexOf
, IndexOfAny
, LastIndexOf
, and LastIndexOfAny
.
The extension methods follow the simple convention of being suffixed with OrNone
.
Option<string> ParseKey(string input)
=> input.IndexOfOrNone('[')
.Select(startIndex => ParseKeyWithMultipleParts(input, startIndex))
.GetOrElse(() => ParseRegularKey(input));
Example usage of IndexOfOrNone
λ0001: Disallowed use of TryGetValue
TryGetValue
should not be used.
Cause
TryGetValue
is used outside one of the allowed use cases.
Reason for rule
TryGetValue
is an advanced API that is needed to interoperate with imperative constructs, such as loops and catch
filter clauses.
Use of this API is restricted to prevent misuse. This is critical since TryGetValue
is essentially a function to get the value out of the option.
How to fix violations
Use one of the functions provided on Option<T>
such as Select
, SelectMany
, Match
or GetOrElse
.
Examples
Disallowed
static void Example(Option<int> option)
{
const int fallback = 42;
var valueOrFallback = option.TryGetValue(out var value) ? value : fallback;
}
Allowed
Iterator
public static IEnumerable<TItem> Successors<TItem>(Option<TItem> first, Func<TItem, Option<TItem>> successor)
where TItem : notnull
{
var item = first;
while (item.TryGetValue(out var itemValue))
{
yield return itemValue;
item = successor(itemValue);
}
}
Catch filter clause
try
{
// ...
} catch (Exception exception) when (FindHandlerForException(exception).TryGetValue(out var handler))
{
handler.Handle(exception);
}
Option<IExceptionHandler> FindHandlerForException(Exception exception) => ...;
Changelog
All notable changes to this project will be documented in this file. Funcky adheres to Semantic Versioning.
Funcky 3.4.0 | Funcky.Async 1.3.0 | Funcky.XUnit 2.0.2
This update is mainly to update to .NET 8 but also has several smaller improvements.
Native AOT
Both Funcky and Funcky.Async have been annotated to be compatible with Native AOT.
The only exception is OptionJsonSerializer
which is not compatible with Native AOT.
.NET 8
We use the new C#12 and .NET features in the code, and expose new features through our API.
- .NET 8 added new overloads to their
TryParse
APIs. These changes are reflected in Funcky'sParseOrNone
APIs.ParseByteOrNone
overloads withReadOnlySpan<byte>
andstring?
ParseSByteOrNone
overloads withReadOnlySpan<byte>
ParseSingleOrNone
overloads withReadOnlySpan<byte>
ParseDoubleOrNone
overloads withReadOnlySpan<byte>
ParseDecimalOrNone
overloads withReadOnlySpan<byte>
ParseInt16OrNone
overloads withReadOnlySpan<byte>
ParseInt32OrNone
overloads withReadOnlySpan<byte>
ParseInt64OrNone
overloads withReadOnlySpan<byte>
ParseUInt16OrNone
overloads withReadOnlySpan<byte>
ParseUInt32OrNone
overloads withReadOnlySpan<byte>
ParseUInt64OrNone
overloads withReadOnlySpan<byte>
ParseNumberOrNone<TNumber>
overloadsParseOrNone<TParsable>
overloads
String Extensions
We implemented a few of the IEnumerable extensions which are very useful on strings.
Chunk
onstring
.SlidingWindow
onstring
.
Monads
- Implemented
UpCast
for the mondsOption
,Either
,Result
andSystem.Lazy
. - Implemented
InspectEmpty
onIEnumerable
andIAsyncEnumerable
- Implemented
ToAsyncEnumerable
extension onOption
IEnumerator
MoveNextOrNone
extension onIEnumerator<T>
Consistency
FindIndexOrNone
andFindLastIndexOrNone
extensions onList
Funcky 3.3.0 | Funcky.Analyzers 1.3.0 | Funcky.Xunit 2.0.1
This is a relatively minor release focuses on convenience for our monads Option
, Either
and Result
.
GetOrElse
and OrElse
for all
We've added GetOrElse
and OrElse
to Either
and Result
bringing them on par with Option
.
The corresponding analyzer now also correctly suggests using these methods instead of Match
for Result
and Either
.
Inspect
for the error case
All three alternative monads Option
, Either
and Result
now support inspecting the «error» case:
Option.InspectNone
- executes a side effect only when the option isNone
.Either.InspectLeft
- executes a side effect only when the either is on theLeft
side.Result.InspectError
- executes a side effect only when the result is anError
.
These methods are particularly useful for logging warnings/errors.
Funcky.XUnit
- Funcky.XUnit is only compatible with XUnit 2.4, this is now correctly declared.
Funcky 3.2.0 | Funcky.Async 1.2.0
List Pattern for Option
We've added support for C# 11's List Patterns to Option<T>
.
This means that you can use regular switch
expressions / statements to match on options:
var greeting = person switch
{
{ FirstName: var firstName, LastName: [var lastName] } => $"Hello {firstName} {lastName}",
{ FirstName: var firstName } => $"Hi {firstName}",
};
record Person(string FirstName, Option<string> LastName);
Discard
The new Discard.__
field provides a short-hand for Unit.Value
to be used with switch
expressions.
using static Funcky.Discard;
return __ switch
{
_ when user.IsFrenchAdmin() => "le sécret",
_ when user.IsAdmin() => "secret",
_ => "(redacted)",
};
Retry with Exception
We've added overloads to the Retry
and RetryAsync
functions that allow retrying a function
as long as an exception is thrown.
Example from IoRetrier
:
// Retries an action until the file is no longer in use.
Task RetryWhileFileIsInUseAsync(IRetryPolicy policy, Action action)
=> RetryAsync(ActionToUnit(action), exception => exception is IOException && exception.HResult == FileInUseHResult, policy);
New Extensions
EnumerableExtensions.MinByOrNone
EnumerableExtensions.MaxByOrNone
ImmutableListExtensions.IndexOfOrNone
ImmutableListExtensions.LastIndexOfOrNone
ListExtensions.IndexOfOrNone
Funcky 3.1.0 | Funcky.Async 1.1.0 | Funcky.Analyzers 1.2.0
New APIs
- ✨
OptionExtensions.ToNullable
✨ StreamExtenions.ReadByteOrNone
- New overloads for
ElementAtOrNone
that take anIndex
. - New overload for
JoinToString
that takes anIEnumerable<string>
.
.NET 7
- .NET 7 added new overloads to their
TryParse
APIs. These changes are reflected in Funcky'sParseOrNone
APIs. - The
ParseOrNone
methods include the new[StringSyntax]
attribute from .NET 7.
Analyzers
The new Option.Match
analyzer suggests simpler alternatives over custom Match
es including
the all-new ToNullable
extension.
Funcky 3.0.0 | Funcky.Async 1.0.0 | Funcky.XUnit 2.0.0
There's a handy Migration Guide available.
New APIs
Result.GetOrThrow
EnumerableExtensions.GetNonEnumeratedCountOrNone
PriorityQueue
PriorityQueueExtensions.DequeueOrNone
PeekOrNone
Traversable
The new Traverse
and Sequence
extension methods allow you to
«swap» the inner and outer monad (e.g. Result<Option<T>>
-> Option<Result<T>>
)
Memoize
The new Memoize
extension function returns an IBuffer
/ IAsyncBuffer
.
This new type represents ownership over the underlying enumerator (and is therefore IDisposable
).
CycleRange
and RepeatRange
have also been changed to return an IBuffer
.
ParseExtensions
The parse extensions have been improved with the goal of aligning more with the BCL. Many of the changes are breaking.
- The functions now use BCL type names instead of C# type names
(e.g.
ParseIntOrNone
has been renamed toParse
) - The parameter names and nullability have been changed to align with the BCL.
- Added
HttpHeadersNonValidatedExtensions
IReadOnlyList
/ IReadOnlyCollection
Funcky now communicates materialization in the IEnumerable<T>
extensions by returning
IReadOnlyList
or IReadOnlyCollection
. This reduces «multiple enumeration» warnings.
Materialize
Chunk
Partition
Shuffle
SlidingWindow
Split
Transpose
Sequence.Return
Disallowing null
Values
Our Option<T>
type has always disallowed null
values.
This has been extended to our other monads: Result<T>
, Either<L, R>
and Reader<E, R>
.
Breaking Changes
Option.None()
has been changed to a property. There's an automatic fix available for this.- Our
Match
functions now differentiate betweenFunc
andAction
. TheAction
overloads have been renamed toSwitch
. - The return type of
EnumerableExtensions.ForEach
has been changed toUnit
. - Many parameter names and generic type names have been renamed in an attempt to unify naming across Funcky.
- All
Action
extensions have been moved to a new classActionExtensions
. EitherOrBoth
has been moved to theFuncky
namespace.- The retry policies have been moved to the
Funcky.RetryPolicies
namespace. Partition
returns a customPartitions
struct instead of a tuple.
Obsoleted APIs Removed
APIs that have been obsoleted during 2.x have been removed:
ObjectExtensions.ToEnumerable
Funcky.GenericConstraints.RequireClass
andRequireStruct
- All
Try*
APIs (TryGetValue
,TryParse*
, etc.). These APIs use theOrNone
suffix instead. Sequence.Generate
has been superceded bySequence.Successors
CartesianProduct
JSON Converter
We have removed the implicit System.Text.Json
converter for Option<T>
.
This means that you'll have to register the OptionJsonConverter
yourself.
⚠️ Test this change carefully as you won't get an error during compilation, but rather at runtime.
Moved to Funcky.Async
All APIs related to IAsyncEnumerable
and Task
have been moved to the new Funcky.Async
package:
AsyncEnumerableExtensions
Functional.RetryAsync
->AsyncFunctional.RetryAsync
Option<Task>
andOption<ValueTask>
awaiters
Funcky.Async
AsyncSequence
This class exposes all of the same factory functions as Sequence
, but for IAsyncEnumerable
:
Return
Successors
Concat
Cycle
CycleRange
FromNullable
RepeatRange
New IAsyncEnumerable
extensions
We've worked hard towards the goal of parity between our extensions for IEnumerable
and IAsyncEnumerable
:
AdjacentGroupBy
AnyOrElse
AverageOrNoneAsync
/MaxOrNoneAsync
/MinOrNoneAsync
Chunk
ConcatToStringAsync
ExclusiveScan
InclusiveScan
Inspect
Interleave
Intersperse
JoinToStringAsync
MaterializeAsync
Memoize
Merge
NoneAsync
PartitionAsync
PowerSet
Sequence
/SequenceAsync
/Traverse
/TraverseAsync
ShuffleAsync
SlidingWindow
Split
Transpose
WhereNotNull
WithIndex
/WithLast
/WithPrevious
/WithFirst
ZipLongest
Funcky.Xunit
- Breaking: The
Is
prefix has been dropped from assertion methods for consistency with XUnit's naming scheme for assertion methods.
Funcky 2.7.1
Deprecations
Option.None<T>()
: We originally introduced theOption.None<T>
method as a future proof replacement toOption<T>.None
for use in method groups, because Funcky 3 changesOption<T>.None
to a property. This turned out to be confusing to users especially because both method are always suggested in autocomplete.
Funcky 2.7.0 | Funcky.XUnit 1.0.0 | Funcky.Analyzers 1.1.0
This release is the last non-breaking release for Funcky before 3.0.
Deprecations
EnumerableExtensions.CartesianProduct
will be removed in Funcky 3.- To align our naming with that of the BCL, the
ParseOrNone
methods that return a type that has a keyword in C#int
,long
, etc. use the name of the BCL type instead.
Example:ParseIntOrNone
becomesParseInt32OrNone
.
The old methods will be removed in Funcky 3. - In preparation for Funcky 3 we deprecated
Option<T>.None
when used as method group. UseOption.None<T>
instead.
New ParseOrNone
extensions
With the help of a source generator we have added a lot of new ParseOrNone methods for various types from the BCL:
- Unsigned integer types
DateOnly
,TimeOnly
Version
- Support for
ReadOnlySpan<T>
as input - ... and more
Convenience for Either
and Result
- Added implicit conversions for
Either
andResult
. - Implement
Inspect
forEither
andResult
. - Added
Partition
forIEnumerable<Either>
andIEnumerable<Result>
. - Added
ToString
onEither
andResult
. - Implement
ToEither
onOption
.
IEnumerable<T>
extensions
AnyOrElse
- Prefix sum:
InclusiveScan
andExclusiveScan
Analyzers
This release adds two new analyzer rules:
- λ1003: Warning when certain methods, such as
Match
are used without argument labels - λ1004: Warning that suggests
.ConcatToString()
over.JoinToString("")
Both of these warnings come with corresponding code fixes.
Funcky.Xunit
- Breaking: Funcky.Xunit now uses the
Funcky
namespace, instead ofFuncky.Xunit
. - Add assertion methods for testing
Either
:FunctionalAssert.IsLeft
andFunctionalAssert.IsRight
.
Funcky 2.6.0 | Funcky.Analyzers 1.0.0
Analyzers
This release comes with a new package Funcky.Analyzers
, which we'll use
to guide users of Funcky
New extensions
- Add extensions
DequeueOrNone
andPeekOrNone
onQueue
andConcurrentQueue
. - Add extension
ConcatToString
as an alias forstring.Concat
. - Add overload to
WhereSelect
with no parameter. - Add methods to convert from
Either
toOption
: #439LeftOrNone
: Returns the left value orNone
if the either value was right.RightOrNone
: Returns the right value orNone
if the either value was left.
- Extension functions for
System.Range
to allow the generations ofIEnumerable<T>
s from Range-Syntax:foreach(var i in 1..5) { } // negative numbers are not supported from x in 5..2 from y in 1..3 select (x, y)
Improvements to Sequence
Sequence.Return
now accepts multiple parameters:Sequence.Return(1, 2, 3)
- ⚠️
Sequence.Generate
has been deprecated in favour of the newly addedSequence.Successors
function which includes the first element (seed) in the generated sequence.
Improvements to Option
- Add
Option.FromBoolean
to create anOption<T>
from a boolean.
Improvements to Result
The behaviour of the Result.Error
constructor has been changed regarding exceptions
with an already set stack trace. The original stack trace is now preserved.
Previously this resulted in the stacktrace being replaced (.NET < 5.0) or an error (.NET ≥ 5.0).
Improvements to Either
- Add
Either.Flip
to swaps left with right.
Tooling
- Funcky automatically adds global usings for the most important namespaces of funcky
when the
FunckyImplicitUsings
property is set. This requires .NET SDK ≥ 6.0 and C# ≥ 10.0. - Funcky now supports trimming for self-contained deployments.
Option<T>
now works with the new System.Text.Json source generation.- The
Funcky
package now supports Source Link and deterministic builds. - The symbols package is now finally working again.
Funcky 2.5.0
Reader Monad
This release includes the Reader
monad including a bunch of factory methods
and convenience extensions.
public static Reader<Enviroment, IEnumerable<string>> DefaultLayout(IEnumerable<DateTime> month)
=> from colorizedMonthName in ColorizedMonthName(month)
from weekDayLine in WeekDayLine()
from weeksInMonth in month
.GroupBy(GetWeekOfYear)
.Select(FormatWeek)
.Sequence()
select BuildDefaultLayout(colorizedMonthName, weekDayLine, weeksInMonth);
Improved Action
Extensions
Funcky now supports Curry
, Uncurry
and Flip
for Action
s too.
This release also adds the inversion of ActionToUnit
: UnitToAction
More Extensions for IEnumerable<T>
Intersperse
: Adds a given item in between all items of an enumerable.JoinToString
: Alias forstring.Join
.WithPrevious
: Similar toWithFirst/Last/Index
but with the predecessor of each item.ForEach
: Add an overload toForEach
that accepts aUnit
-returningFunc
.
Additional Factory Methods
EitherOrBoth.FromOptions
creates anEitherOrBoth
from two options.Lazy.FromFunc
creates aLazy<T>
from aFunc
.
This is sugar over theLazy<T>
constructor, with the additional benefit of supporting type inference.Lazy.Return
creates aLazy<T>
from a value.
This is sugar over theLazy<T>
constructor, with the additional benefit of supporting type inference.
Documentation Improvements
This release comes with a few small documentation improvements.
Funcky users will now also see the [Pure]
attributes which were previously not emitted.
Funcky 2.4.1
- Remove upper bounds on all Microsoft.Bcl.* dependencies. Between the 2.3.0 and 2.4.0 release an overly restrictive upper bound was accidentally introduced for Microsoft.Bcl.AsyncInterfaces.
Funcky 2.4.0
Try*
→ *OrNone
We've renamed all Try*
methods, such as TryParse
, TryGet
value to *OrNone
.
The old methods are still available, but marked as obsolete and will be removed in 3.0.0.
Factory methods for IEnumerable<T>
This release adds some new factory methods for creating IEnumerable<T>
to the Sequence
class:
Sequence.RepeatRange
: Generates a sequence that contains the same sequence of elements the given number of timesSequence.Cycle
: Cycles the same element over and over again as an endless generator.Sequence.CycleRange
: Generates a sequence that contains the same sequence of elements over and over again as an endless generatorSequence.Concat
More Extension Methods
for IEnumerable<T>
Materialize
: Materializes all the items of a lazy enumerable.PowerSet
: Returns a sequence with the set of all subsetsShuffle
: Returns the given sequence in random Order in O(n).Split
: Splits the source sequence a separator.ZipLongest
: Zips two sequences with different lengths.
for string
SplitLazy
: Splits a string by separator lazily.SplitLines
: Splits a string by newline lazily.
for Func
Curry
Uncurry
Flip
Compose
EitherOrBoth
EitherOrBoth is a new data type that can represent Left
, Right
and Both
. It is used in ZipLongest
.
Monad.Return
This release adds a Return
method for all monad types in Funcky:
Option.Return
Either<TLeft>.Return
Result.Return
OptionEqualityComparer
To support more advanced comparison scenarios, OptionEqualityComparer
has been added similar to the already existing OptionComparer
.
Smaller Improvements
- Added a missing
Match
overload toEither
that takesAction
s - Added additional overloads for
Functional.True
andFunctional.False
for up to four parameters.
Funcky 2.3.0
net5.0
has been added to Funcky's target frameworks.
Improvements to Option<T>
Option<T>
is now implicitly convertible fromT
.public static Option<int> Answer => 42;
Option
adds support forSystem.Text.Json
:
The customJsonConverter
is picked up automatically when serializing/deserializing.None
is serialized asnull
andSome(value)
is serialized to whatevervalue
serializes to.
Factory methods for IEnumerable<T>
This release adds factory methods for creating IEnumerable<T>
with the static class Sequence
:
Sequence.Return
: Creates anIEnumerable<T>
with exactly one item.Sequence.FromNullable
: Creates anIEnumerable<T>
with zero or one items.Sequence.Generate
: Creates anIEnumerable<T>
using a generation function and a seed.
More Extension Methods for IEnumerable<T>
This release adds a bunch of new extension methods on IEnumerable<T>
:
AdjacentGroupBy
AverageOrNone
CartesianProduct
Chunk
ElementAtOrNone
Interleave
MaxOrNone
Merge
MinOrNone
Pairwise
Partition
SlidingWindow
TakeEvery
Transpose
WithFirst
WithIndex
WithLast
IAsyncEnumerable<T>
Support
This release adds a couple of extension methods that provide interoperability
with Option<T>
to IAsyncEnumerable<T>
:
WhereSelect
FirstOrNoneAsync
LastOrNoneAsync
SingleOrNoneAsync
ElementAtOrNoneAsync
A couple of the new extension methods on IEnumerable<T>
have async counterparts:
Pairwise
TakeEvery
The naming of the extension methods and their overloads follows that of System.Linq.Async
.
Improved IQueryable
Support
This release adds specialized extension methods for IQueryable<T>
that are better
suited especially for use with EF Core:
FirstOrNone
LastOrNone
SingleOrNone
Dependencies
To support .NET Standard, Funcky conditionally pulls in dependencies that provide the missing functionality:
Microsoft.Bcl.AsyncInterfaces
for .NET Standard 2.0System.Collections.Immutable
andSystem.Text.Json
for .NET Standard 2.0 and 2.1- The version constraints for all these packages have been relaxed to allow 5.x.
Improvements
ConfigureAwait(false)
is now used everywhereawait
is used.- The
IRetryPolicy
implementations now use correctTimespan
withdouble
multiplication when targeting .NET Standard 2.0.
Deprecations
ObjectExtensions.ToEnumerable
has been deprecated in favor ofSequence.FromNullable
.RequireClass
andRequireStruct
have been obsoleted with no replacement.
Funcky 2.2.0 | Funcky.xUnit 0.1.3
- Added overload to
Functional.Retry
with aIRetryPolicy
. - Added
None
overload that takes no predicate.
Funcky 2.1.1 | Funcky.xUnit 0.1.2
- Re-release of previous release with correct assemblies.
Funcky 2.1.0 | Funcky.xUnit 0.1.1
- Add
Inspect
method toOption
akin toIEnumerable.Inspect
. - Add
ToTheoryData
extension forIEnumerable<T>
for xUnit. - Add
Unit.Value
as a way to a get aUnit
value. - Add
Functional.Retry
which retries a producer untilOption.Some
is returned.
Funcky 2.0.0
Breaking Changes
- Remove
Reader
monad based onawait
. - Remove
IToString
. - Remove overload for
Option.From
that flattens passedOption
s. - Move
ToEnumerable
extension method to its own class. This is only a breaking change if you've used the extension method as normal method. In that case you need to changeEnumerableExtensions.ToEnumerable
toObjectExtensions.ToEnumerable
. - Rename
Option.From
toOption.FromNullable
and remove overload that takes non-nullable value types. - Unify
Option<T>.ToEnumerable
andYield
toToEnumerable
- Rename
OrElse
overloads that return the item toGetOrElse
which improves overload resolution. - The
Each
extension method onIEnumerable<T>
has been renamed toForEach
. - Move the
Ok
constructor ofResult<T>
to a non-generic class. This allows for the compiler to infer the generic type. Old:Result<int>.Ok(10)
. New:Result.Ok(10)
. - Use
Func<T, bool>
instead ofPredicate<T>
in predicate composition functions (Functional.All
,Functional.Any
,Functional.Not
), because most APIs inSystem
useFunc
. Functional.Any
now returnsfalse
when the given list of predicates is empty.
Fixes
- Fix incorrect
Equals
implementation onOption
.Equals
previously returnedtrue
when comparing aNone
value with aSome
value containing the default value of the type. Exception
created byResult
monad contains valid stack trace- Fix incorrect implementation on
Result.SelectMany
which called theselectedResultSelector
even when the result was an error. As a result (pun intended) of the fix,ResultCombinationException
is no longer needed and also removed.
Additions
- Add
IndexOfOrNone
,LastIndexOfOrNone
,IndexOfAnyOrNone
andLastIndexOfAnyOrNone
extension methods tostring
. - Added
Curry
,Uncurry
andFlip
to theFunctional
Class - Add extension method for
HttpHeaders.TryGetValues
, which returns anOption
. - Add extension methods for getting
Stream
properties that are not always available, asOption
:GetLengthOrNone
,GetPositionOrNone
,GetReadTimeoutOrNone
,GetWriteTimeoutOrNone
. - Add
None
extension method toIEnumerable
. Option<Task<T>>
,Option<Task>
and theirValueTask
equivalents are now awaitable:var answer = await Option.Some(Task.FromResult(42));
Improvements
- Full nullable support introduced with C# 8.
- Mark our functions as
[Pure]
. - Implement
IEquatable
onOption
,Result
andEither
.
Funcky 2.0.0-rc.2
- Move the
Ok
constructor ofResult<T>
to a non-generic class. This allows for the compiler to infer the generic type. Old:Result<int>.Ok(10)
. New:Result.Ok(10)
. - Add
IndexOfOrNone
,LastIndexOfOrNone
,IndexOfAnyOrNone
andLastIndexOfAnyOrNone
extension methods tostring
. - Rename
OrElse
overloads that return the item toGetOrElse
which improves overload resolution. - Added
Curry
,Uncurry
andFlip
to theFunctional
Class - Remove
IToString
. - Mark our functions as
[Pure]
. - Fix incorrect implementation on
Result.SelectMany
which called theselectedResultSelector
even when the result was an error. As a result (pun intended) of the fix,ResultCombinationException
is no longer needed and also removed.
Funcky 2.0.0-rc.1
- Full nullable support introduced with C# 8
- Rename
Option.From
->Option.FromNullable
and remove overload that takes non-nullable value types. - Use
Func<T, bool>
instead ofPredicate<T>
in predicate composition functions (Functional.All
,Functional.Any
,Functional.Not
), because most APIs inSystem
useFunc
. Functional.Any
now returnsfalse
when the given list of predicates is empty.- The
Each
extension method onIEnumerable<T>
has been renamed toForEach
. - Unify
Option<T>.ToEnumerable
andYield
toToEnumerable
- Remove
Reader
monad based onawait
. Exception
created byResult
monad contains valid stack trace
Funcky 1.8.0
- Added overload for
AndThen
which flattens theOption
- Add
Where
method toOption<T>
, which allows filtering theOption
by a predicate. - Add overload for
Option<T>.SelectMany
that takes only a selector. - Add
WhereNotNull
extension method forIEnumerable<T>
.
Funcky 1.7.0
- Add nullability annotations to everything except for
Monads.Reader
. - Add a function for creating an
Option<T>
from a nullable value:Option.From
. Either.Match
now throws when called on anEither
value created usingdefault(Either<L, R>)
.- Add
True
andFalse
functions to public API - Match of
Result
Monad accepts actions - Add
FirstOrNone
,LastOrNone
andSingleOrNone
extension functions
Funcky 1.6.0
- Add ToEnumerable function to
Option<T>
. - Add
WhereSelect
extension function forIEnumerable<T>
. - Add missing overload for nullary actions to
ActionToUnit
.