RustyOptions.FSharp
0.7.0
See the version list below for details.
dotnet add package RustyOptions.FSharp --version 0.7.0
NuGet\Install-Package RustyOptions.FSharp -Version 0.7.0
<PackageReference Include="RustyOptions.FSharp" Version="0.7.0" />
paket add RustyOptions.FSharp --version 0.7.0
#r "nuget: RustyOptions.FSharp, 0.7.0"
// Install RustyOptions.FSharp as a Cake Addin #addin nuget:?package=RustyOptions.FSharp&version=0.7.0 // Install RustyOptions.FSharp as a Cake Tool #tool nuget:?package=RustyOptions.FSharp&version=0.7.0
RustyOptions
Option and Result types for C#, inspired by Rust
dotnet add package RustyOptions
Avoid Null-Reference Errors
The C# nullable reference types feature is useful, but it's entirely optional, and the warnings it produces are easily ignored.
RustyOptions uses the type system to:
- Make it impossible to access a possibly-missing value without first checking if the value is present.
- Clearly express your intent in the code you build. If a method might not return a value, or might return an error message instead of a value, you can express this in the return type where it can't be missed.
- Provide an expressive API that allows chaining and combination of optional or possibly-failed results.
Usage
Creating an Option
There are many ways to create an Option
instance - use whichever appeals to you!
Option<int> ex1 = Option.Some(42);
Option<int> ex2 = 42.Some();
Option<int> ex3 = new Option<int>(42);
Option<int> ex4 = Option<int>.None;
Option<int> ex5 = Option.None<int>();
Option<int> ex6 = default; // equivalent to Option.None<int>()
int? maybeNull = GetPossiblyNullInteger();
Option<int> ex7 = Option.Create(maybeNull); // null turns into Option.None
// Or you can use 'using static' for more concise/F#/Rust-like syntax:
using static RustyOptions.Option;
var ex8 = Some(42);
var ex9 = None<int>();
Getting values from an Option
if (myOption.IsSome(out var value))
{
// do something with the value
}
// Get the contained value if Some, or the default value for the contained type if None
var innerValue = myOption.UnwrapOr(default);
if (myOption.IsNone)
{
// Do something because the option is None,
// although in many cases the Unwrap/Map/And/Or
// methods are more useful than checking IsNone directly.
}
Creating a Result
Result
can also be created in a variety of ways.
// assumes an Err type of string
Result<int, string> ex1 = Result.Ok(42);
// assumes an Err type of Exception
Result<int, Exception> ex2 = Result.OkExn(42);
// fully specify Ok and Err types
Result<int, MyCustomException> ex3 = Result.Ok<int, MyCustomException>(42);
Result<int, string> ex4 = Result.Err<int>("Oops.");
Result<int, Exception> ex5 = Result.Err<int>(new Exception("Oops."));
Result<int, MyCustomException> ex6 = Result.Err<int, MyCustomException(someException);
// Or you can use 'using static' for more concise/F#/Rust-like syntax:
using static RustyOptions.Result;
var ex7 = Ok(42);
var ex8 = Err<int>("oops");
Getting values from a Result
if (myResult.IsOk(out var value))
{
// do something with the value
}
if (myResult.IsErr(out var err))
{
// do something with the error
}
Safely Chain Together Fallible Methods
RustyOptions has an extensive API
that supports safely chaining together multiple methods that return Option
or Result
and supports easily converting between Option
and Result
if you have a mixture of such methods.
var output = Option.Parse<int>(userInput)
.AndThen(ValidateRange)
.OrElse(() => defaultsByGroup.GetValueOrNone(user.GroupId))
.MapOr(PillWidget.Render, string.Empty);
The example above does the following:
- Attempts to parse the user input into an integer.
- If the parsing succeeds, passes the resulting number to the
ValidateRange
method, which returnsSome(parsedInput)
if the parsed input is within the valid range, orNone
if it falls outside the valid range. - If either steps 1 or 2 fail, we attempt to do a dictionary lookup to get a default value using the current user's group ID.
- If at the end we have a value, we render it to a string. Otherwise, we set
output
to an empty string.
Parsing (.NET 7+ only)
Any current or future type that supports IParsable<T>
or ISpanParsable<T>
can be conditionally parsed into an Option<T>
or NumericOption<T>
.
Option<int> integer = Option.Parse<int>("12345");
Option<DateTime> date = Option.Parse<DateTime>("2023-06-17");
Option<TimeSpan> timespan = Option.Parse<TimeSpan>("05:11:04");
Option<double> fraction = Option.Parse<double>("3.14");
Option<Guid> guid = Option.Parse<Guid>("ac439fd6-9b64-42f3-bc74-38017c97b965");
Option<int> nothing = Option.Parse<int>("foo");
Generic Math (.NET 7+ only)
Doing math with NumericOption<int>
is similar to doing math with Nullable<int>
. Anything combined with None
comes out as None
, but if all values involved are Some
then the math operations are performed as normal on the inner type, and results are wrapped in Some
.
using static RustyOptions.NumericOption;
var a = Some(3);
var b = Some(5);
Assert.Equal(Some(15), a * b);
// Implicit conversion allows you to mix raw numbers and options in the same expression:
Assert.Equal(Some(25), b * 5);
// You can even use Option<int> as the index value in a for loop!
for (var i = Some(0); i < 5; i++)
{
// If you set i to None inside the loop,
// the loop will exit when the current iteration completes,
// as None is not less than 5.
i = None<int>();
}
Uses Modern .NET Features
For performance and convenience:
- Supports parsing any type that implements
IParsable<T>
orISpanParsable<T>
(.NET 7 and above only) - The
NumericOption<T>
type supports generic math for any contained type that implementsINumber<T>
(.NET 7 and above only) - Supports
async
andIAsyncEnumerable<T>
. - Supports nullable type annotations.
- Supports serialization and deserialization with
System.Text.Json
. IEquatable<T>
andIComparable<T>
allowOption
andResult
types to be easily compared and sorted.IFormattable
andISpanFormattable
allowOption
andResult
to efficiently format their content.Option
andResult
can be efficiently converted toReadOnlySpan<T>
orIEnumerable<T>
for easier interop with existing code.- Convenient extension methods for working with dictionaries (
GetValueOrNone
), collections (FirstOrNone
), enums (Option.ParseEnum
) and more. - Supports explicit conversion to and from the F# Option, ValueOption, and Result types for easy interop.
FAQ
- This library only supports .NET 6 and above. What about .NET Framework? .NET 5? .NET Core 3.1?
- You may want to consider the Optional library for legacy framework support.
- .NET Core 3.1 and .NET 5 are not supported because as of this writing they are no longer supported by Microsoft. However, if these runtimes are important to you, we welcome pull requests.
- Why create this library if Optional already exists?
- I prefer the Rust Option/Result API methods and wanted to replicate those in C#.
- I wanted to take advantage of modern .NET features like
ISpanParsable<T>
andINumber<T>
. - I think having distinct
Option
andResult
types is more clear than having two different kinds of Option. - As of this writing, Optional hasn't been updated in five years.
- What about F#
Option
andResult
?- The
RustyOptions.FSharp
nuget package will allow you to convert between F# and RustyOptions types.
- The
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net6.0 is compatible. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 is compatible. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
-
net6.0
- FSharp.Core (>= 6.0.0)
- RustyOptions (>= 0.7.0)
-
net7.0
- FSharp.Core (>= 7.0.0)
- RustyOptions (>= 0.7.0)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.