Skip to content

Commit

Permalink
Updated documentation for release 1.2.
Browse files Browse the repository at this point in the history
  • Loading branch information
Salvatore Isaja committed Mar 26, 2023
1 parent ac6172a commit 686cc31
Show file tree
Hide file tree
Showing 4 changed files with 58 additions and 4 deletions.
4 changes: 2 additions & 2 deletions DiscriminatedOnions.Tests/DiscriminatedOnions.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
</PropertyGroup>

<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.5.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.1.0" />
<PackageReference Include="FluentAssertions" Version="6.10.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.5.0" />
<PackageReference Include="NUnit" Version="3.13.3" />
</ItemGroup>

Expand Down
21 changes: 21 additions & 0 deletions DiscriminatedOnions.Tests/ReadMeExamples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN
*/

using System.Collections.Generic;
using System.Threading.Tasks;
using FluentAssertions;
using NUnit.Framework;

Expand All @@ -50,6 +51,16 @@ public static void Option_DefaultValue()
defaulted.Should().Be("default value");
}

[Test]
public static async Task Option_BindAsync_Some()
{
Option<string> someString = Option.Some("I have a value");
Option<string> asyncBound = await someString
.BindAsync(v => Task.FromResult(Option.Some(v + " altered")))
.Pipe(o => o.BindAsync(v => Task.FromResult(Option.Some(v + " two times"))));
asyncBound.Should().Be(Option.Some("I have a value altered two times"));
}

[Test]
public static void Option_Choose()
{
Expand Down Expand Up @@ -91,6 +102,16 @@ public static void Result_Bind_Error()
boundError.Should().Be(Result.Error<string, int>(42));
}

[Test]
public static async Task Result_BindAsync_Ok()
{
Result<string, int> ok = Result.Ok<string, int>("result value");
Result<string, int> asyncBoundOk = await ok
.BindAsync(v => Task.FromResult(Result.Ok<string, int>("beautiful " + v)))
.Pipe(o => o.BindAsync(v => Task.FromResult(Result.Ok<string, int>("very " + v))));
asyncBoundOk.Should().Be(Result.Ok<string, int>("very beautiful result value"));
}

[Test]
public static void Pipe()
{
Expand Down
4 changes: 2 additions & 2 deletions DiscriminatedOnions/DiscriminatedOnions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<Nullable>enable</Nullable>
<Version>1.1.0</Version>
<Version>1.2.0</Version>
<Authors>Salvo Isaja</Authors>
<Company>Salvo Isaja</Company>
<Copyright>Copyright 2022 Salvatore Isaja</Copyright>
<Copyright>Copyright 2022-2023 Salvatore Isaja</Copyright>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<PackageProjectUrl>https://github.com/salvois/DiscriminatedOnions</PackageProjectUrl>
<Description>A stinky but tasty hack to emulate F#-like discriminated unions in C#</Description>
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ I have written this library because I needed it and because it was fun, but it d

## Table of contents

- [Changelog](#changelog)
- [Rolling your own discriminated unions](#rolling-your-own-discriminated-unions)
- [Reference type vs. value type discriminated unions](#reference-type-vs-value-type-discriminated-unions)
- [Option type](#option-type)
Expand All @@ -23,6 +24,12 @@ I have written this library because I needed it and because it was fun, but it d
- [Piping function calls](#piping-function-calls)
- [License](#license)

## Changelog

* 1.2: Async versions of `bind`, `iter` and `map` for `Option` and `Result`
* 1.1: `Option`-based `TryGetValue` for dictionaries
* 1.0: Finalized API with `Option` and `Result` reimplemented as value types for better performance

## Rolling your own discriminated unions

If you are here, you probably know better than me what a discriminated union is :) \
Expand Down Expand Up @@ -118,6 +125,8 @@ A `ToString` override provides representations such as `"Some(Value)"` or `"None

Together with the `Option<T>` type itself, a companion static class `Option` of utility functions is provided. For a full description of those functions please refer to the official documentation of the [F# Option module](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-optionmodule.html).

Moreover, some novel functions not included in the F# standard library are included, such as async versions that may be useful when working with I/O.

The `Option` static class also provides named constructors to create `Option<T>` values. The named constructor for `Some` helps the compiler deduce the generic argument for `T` reducing some bolilerplate, whereas the named constructor for `None` returns a singleton instance for `T`.

```csharp
Expand All @@ -132,6 +141,7 @@ public static class Option

// Returns binder(v) if option is Some(v) or None if it is None
public static Option<U> Bind<T, U>(this Option<T> option, Func<T, Option<U>> binder);
public static Task<Option<U>> BindAsync<T, U>(this Option<T> option, Func<T, Task<Option<U>>> binder);

// Returns true if option is Some(value) or false if it is None
public static bool Contains<T>(this Option<T> option, T value);
Expand Down Expand Up @@ -171,9 +181,11 @@ public static class Option

// Executes action(v) if option is Some(v)
public static void Iter<T>(this Option<T> option, Action<T> action);
public static Task IterAsync<T>(this Option<T> option, Func<T, Task> action);

// Returns Some(mapping(v)) if option is Some(v) or None if it is None
public static Option<U> Map<T, U>(this Option<T> option, Func<T, U> mapping);
public static Task<Option<U>> MapAsync<T, U>(this Option<T> option, Func<T, Task<U>> mapping);

// Returns Some(mapping(v1, v2)) if options are Some(v1) and Some(v2) or None if at least one is None
public static Option<U> Map2<T1, T2, U>(this (Option<T1>, Option<T2>) options, Func<T1, T2, U> mapping);
Expand Down Expand Up @@ -213,7 +225,13 @@ Option<string> mapped = someString.Map(v => v + " today");
Option<string> noString = Option.None<string>();
string defaulted = noString.DefaultValue("default value");
// returns "default value"
Option<string> asyncBound = await someString
.BindAsync(v => Task.FromResult(Option.Some(v + " altered")))
.Pipe(o => o.BindAsync(v => Task.FromResult(Option.Some(v + " two times"))));
// returns Option.Some("I have a value altered two times")
```
Note how [Pipe](#piping-function-calls) has been used in the last example to remove async impedance mismatch.

### Utility functions for IEnumerable involving the Option type

Expand Down Expand Up @@ -317,6 +335,8 @@ A `ToString` override provides representations such as `"Ok(ResultValue)"` or `"

Together with the `Result<T, TError>` type itself, a companion static class `Result` of utility functions is provided. Please refer to the official documentation of the [F# Result module](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-core-resultmodule.html) for a full descrition of them.

Moreover, some novel functions not included in the F# standard library are included, such as async versions that may be useful when working with I/O.

The `Bind` function is especially very convenient when chaining functions when the result of the previous one becomes the input of the next one, something described as [Railway oriented programming](https://fsharpforfunandprofit.com/posts/recipe-part2/) in the famous [F# for Fun and Profit](https://fsharpforfunandprofit.com/) site.

```csharp
Expand All @@ -331,12 +351,15 @@ public static class Result

// Returns binder(v) if result is Ok(v) or Error(e) if it is Error(e)
public static Result<U, TError> Bind<T, TError, U>(this Result<T, TError> result, Func<T, Result<U, TError>> binder);
public static Task<Result<U, TError>> BindAsync<T, TError, U>(this Result<T, TError> result, Func<T, Task<Result<U, TError>>> binder);

// Returns Ok(mapping(v)) is result is Ok(v) or Error(e) if it is Error(e)
public static Result<U, TError> Map<T, TError, U>(this Result<T, TError> result, Func<T, U> mapping);
public static Task<Result<U, TError>> MapAsync<T, TError, U>(this Result<T, TError> result, Func<T, Task<U>> mapping);

// Returns Error(mapping(e)) if result is Error(e) or Ok(v) if it is Ok(v)
public static Result<T, U> MapError<T, TError, U>(this Result<T, TError> result, Func<TError, U> mapping);
public static Task<Result<T, U>> MapErrorAsync<T, TError, U>(this Result<T, TError> result, Func<TError, Task<U>> mapping);
}

Result<string, int> ok = Result.Ok<string, int>("result value");
Expand All @@ -346,7 +369,13 @@ Result<string, int> boundOk = ok.Bind(v => Result.Ok<string, int>("beautiful " +
Result<string, int> error = Result.Error<string, int>(42);
Result<string, int> boundError = error.Bind(v => Result.Ok<string, int>("beautiful " + v));
// returns Result.Error<string, int>(42), short-circuiting
Result<string, int> asyncBoundOk = await ok
.BindAsync(v => Task.FromResult(Result.Ok<string, int>("beautiful " + v)))
.Pipe(o => o.BindAsync(v => Task.FromResult(Result.Ok<string, int>("very " + v))));
// returns Result.Ok<string, int>("very beautiful result value")
```
Note how [Pipe](#piping-function-calls) has been used in the last example to remove async impedance mismatch.

## Choice types

Expand Down Expand Up @@ -377,6 +406,8 @@ public readonly record struct OrderId(int Value);

This library provides no features to create such types, because the built-in feature of the language can be leveraged, but I encourage to try this technique to let the compiler stop you when you try to shoot yourself. A properly modeled domain can dramatically reduce nasty bugs caused by the so called "primitive obsession".

**Caveat:** Instances of record structs may be created using the parameterless constructor, such as `new CustomerId()`, in that case the wrapped value will be initialized to its default value. This is built into the language and cannot be prevented, but should generally be avoided with the approach proposed here, because you could, for example, create a wrapped Svalue even if the wrapped type is a non-nullable reference type.

## Unit type

OK, this is not a discriminated union, but we may need it when we work with generic functions. If you ever had to provide two nearly identical implementations, one taking `Action` and one taking `Func`, just because you cannot write `Func<void>` you know what I mean.
Expand Down Expand Up @@ -438,6 +469,8 @@ int maybePiped = twenty.PipeIf(v => v < 10, v => v * 2);

Both `Pipe` and `PipeIf` provide four overloads (not shown here for conciseness), where either `previous` or `next` are a `Task` that must be awaited, allowing mixed pipelines of synchronous and asynchronous steps.

The async versions of `Pipe` may also be used to chain synchronous and asynchronous function involving `Option`s and `Result`s (see their examples).

## License

Permissive, [2-clause BSD style](https://opensource.org/licenses/BSD-2-Clause)
Expand Down

0 comments on commit 686cc31

Please sign in to comment.