Skip to content

Commit

Permalink
Merge pull request #4 from thinker227/docs
Browse files Browse the repository at this point in the history
Add documentation
  • Loading branch information
thinker227 authored Jan 14, 2024
2 parents 3eb0238 + 2cdb9b7 commit 32f32d0
Show file tree
Hide file tree
Showing 32 changed files with 927 additions and 81 deletions.
46 changes: 46 additions & 0 deletions .github/workflows/publish-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Publish Docs

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: true

on:
workflow_dispatch:
workflow_run:
workflows:
- Release
types:
- completed

jobs:
publish-docs:

runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Install .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Install Docfx
run: dotnet tool install docfx -g
- name: Build documentation
run: docfx docs/docfx.json
- name: Setup Pages
uses: actions/configure-pages@v3
- name: Upload artifact
uses: actions/upload-pages-artifact@v1
with:
path: docs/_site
- name: Deploy to Github Pages
id: deployment
uses: actions/deploy-pages@v1
200 changes: 135 additions & 65 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,123 +6,193 @@

<br></br>

Rascal is a simple and lightweight [result type](https://www.youtube.com/watch?v=srQt1NAHYC0&t=1018s) implementation for C#, containing a variety of utilities and standard functions for working with result types and integrating them into the rest of C#.
Rascal is a simple yet powerful [result type](https://www.youtube.com/watch?v=srQt1NAHYC0&t=1018s) implementation for C#, containing a variety of utilities and standard functions for working with result types and integrating them into the rest of C#.

Rascal is first and foremost an aggregate of the result types I personally find myself implementing in a majority of my own projects, and a competetor other result libraries second. As such, this library implements some things I think other result implementations are lacking, while omitting some features other libraries do implement.

**TODO**: Link to the Github Pages docfx documentation.

<br/>

# Installation

<details>
<summary>.NET CLI</summary>

Run in a terminal in the root of your project:

```ps1
# .NET CLI
dotnet add package Rascal --prerelease
```

# Using the package
</details>

## The prelude
<details>
<summary>Package manager console</summary>

Rascal contains a `Prelude` class (named in reference to most functional languages) which contains a wide variety of utility functions. Since this class contains functions which are used very frequently in code heavily utilizing results, this class is meant to be *imported statically*, i.e. through a `using static` statement. For convenience, this can be included as a global using in a `Usings.cs` file containing other global using statements.
Run from the Visual Studio Package Manager console:

## Samples
```ps1
NuGet\Install-Package Rascal -IncludePrerelease
```

### Creating a result
```cs
// Through explicit Ok/Error functions
var explicitOk = Ok("uwu");
var explicitErr = Err<int>("An error occured.");
</details>

// Through implicit conversion
Result<string> implicitOk = "owo";
```
<details>
<summary>Script environment</summary>

### Mapping a result
In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [RoslynPad](https://roslynpad.net), enter:

"Mapping" refers to taking a result containing some value of type `T` and *mapping* said value to a value of some other type `TNew`.
```cs
// Read input, parse to int, and apply a function to the value
var x = ParseR<int>(Console.ReadLine()!)
.Map(x => Enumerable.Repeat("hewwo", x));
#r "nuget: Rascal"
```

<br/>
If you wish to install a specific version of the package, specify the package version:

Another operation, quite similar to mapping, exists, known as a "bind". A bind operation acts like a map, but the function applied to the value of type `T` returns another result, namely a `Result<TNew>`, which is then returned from the bind. This is the fundamental mechanism which allows chaining result operations together, making for a quite powerful tool.
```cs
// Read input, parse to int, and apply a function to the value, which may fail
var num = ParseR<int>(Console.ReadLine()!);
var den = ParseR<int>(Console.ReadLine()!);
#r "nuget: Rascal, 1.0.1-pre"
```

var val = num.Then(a => den
.Map(b => DiveSafe(a, b)));
</details>

static Result<int> DivSafe(int a, int b) =>
b != 0
? a / b
: "Cannot divide by 0.";
```
<details>
<summary><code>PackageReference</code></summary>

The above expression for `val` can alternatively be written using query syntax:
```cs
var val =
from a in num
from b in den
from x in DivSafe(a, b)
select x;
Add under an `ItemGroup` node in your project file:

```xml
<PackageReference Include="Rascal" Version="1.0.1-pre" />
```

Obviously, replace `1.0.1-pre` with the actual package version you want.

</details>

<br/>

### Various utilities
# Samples

## Creating results

<!-- Github annoyingly does not allow embedding code snippets from other files in markdown, so this entire section is copy-pasted from ./docs/samples/index.md and the ./samples/ folder. REMEMBER TO UPDATE THIS WHEN EDITING THE CORRESPONDING SAMPLES. -->

Results in Rascal can be created in a variety of ways, the two most common of which are through the `Ok` and `Err` methods defined in the prelude, or through implicitly converting ok values or errors into results.

Parse a string or `ReadOnlySpan<char>` to another type, returning a result. `ParseR` (short for `ParseResult`) works for any type implementing `IParsable<TSelf>` or `ISpanParsable<TSelf>`.
```cs
var parsed = ParseR<int>(Console.ReadLine()!);
// You can create a result either through explicit Ok/Error functions...
var explicitOk = Ok(new Person("Melody", 27));
var explicitError = Err<Person>("Could not find person");

// ... or through implicit conversions...
Result<Person> implicitOk = new Person("Edwin", 32);
Result<Person> implicitError = new StringError("Failed to find person");
```

<br/>
## Mapping

"Mapping" refers to taking a result containing some value some type (`T`) and *mapping* said value to a new value of some other type (`TNew`). The principal method of mapping is the aptly named `Map`.

Turn a nullable value into a result.
```cs
var result = F().NotNull();
var name = "Raymond";

// Read console input and try parse it into an int.
// If the input cannot be parsed, the result will be an error.
var age = ParseR<int>(Console.ReadLine()!);

static int? F();
// Map the age to a new person.
// If the age is an error, the person will also be an error.
var person = age.Map(x => new Person(name, x));
```

<br/>

A function for running another function in a `try` block and returning a result containing either the successful value of the function or the thrown exception. Quite useful for functions which provide no good way of checking whether success is expected before running it, such as IO. `Try` variants are also available for `Map` and `Then`.
Another operation, commonly referred to as "bind" or "chaining", exists, which looks quite similar to mapping, the only difference being that the lambda you supply to the method returns a *new* result rather than a plain value. The principal method of chaining is `Then`, which can be read as "a, then b, then c".

```cs
var result = Try(() => File.ReadAllText(path));
// Read console input and assert that it isn't null.
// If the input is null, the value will be an error.
var name = Console.ReadLine().NotNull();

// Chain an operation on the name which will only execute if the name is ok.
var person = name.Then(n =>
{
// Read console input, assert that it isn't null, then try parse it into an int.
var age = Console.ReadLine().NotNull()
.Then(str => ParseR<int>(str));

// Map the age into a new person.
return age.Map(a => new Person(n, a));
});
```

<br/>

Validate inputs directly inside a result expression chain, replacing the original value with an error if the predicate fails.
`Map` and `Then` together make up the core of the `Result<T>` type, allowing for chaining multiple operations on a single result. In functional terms, these are what makes `Result<T>` a functor and monad respectively (although not an applicative).

### Combine

`Combine` is an addition to `Map` and `Then` which streamlines the specific case where you have two results and want to *combine* them into a single result only if both results are ok.

```cs
var input = ParseR<int>(Console.ReadLine()!)
// Read console input and assert that it isn't null.
var name = Console.ReadLine().NotNull();

// Read console input, assert that it isn't null, then try parse it into an int.
var age = Console.ReadLine().NotNull()
.Then(str => ParseR<int>(str));

// Combine the name and age results together, then map them into a person.
var person = name.Combine(age)
.Map(v => new Person(v.first, v.second));
```

## Validation

Rascal supports a simple way of validating the value of a result, returning an error in case the validation fails.

```cs
// Read console input, assert that it isn't null, and validate that it matches the regex.
var name = Console.ReadLine().NotNull()
.Validate(
x => x >= 0,
x => $"Input {x} is less than 0.")

// can also be written as
var input =
from x in ParseR<int>(Console.ReadLine()!)
where x >= 0
select x;
str => Regex.IsMatch(str, "[A-Z][a-z]*"),
_ => "Name can only contain characters a-z and has to start with a capital letter.");

var person = name.Then(n =>
{
// Read console input, assert that it isn't null, try parse it into an int, then validate that it is greater than 0.
var age = Console.ReadLine().NotNull()
.Then(str => ParseR<int>(str))
.Validate(
x => x > 0,
_ => "Age has to be greater than 0.");

return age.Map(a => new Person(n, a));
});
```

<br/>
## Exception handling

### "Unsafe" operations
One of the major kinks of adapting C# into a more functional style (such as using results) is the already existing standard of using exceptions for error-handling. Exceptions have *many* flaws, and result types explicitly exist to provide a better alternative to exceptions, but Rascal nontheless provides a way to interoperate with traditional exception-based error handling.

The `Try` method in the prelude is the premiere exception-handling method, which runs another function inside a `try`-`catch` block, and returns an `ExceptionError` in case an exception is thrown.

To not be *too* far out-of-line with the rest of C#, there are also functions for accessing the values inside results in an unsafe manner. "Unsafe" in this context is not referring to the `unsafe` from C#, but rather the fact these functions may throw exceptions, as opposed to most other functions which are pure and should not normally throw exceptions. These functions should be treated with care and only be used in situations where the caller knows without a reasonable shadow of a doubt that the operation is safe or an exception is acceptable to be thrown.
```cs
Result<int> result;
// Try read console input and use the input to read the specified file.
// If an exception is thrown, the exception will be returned as an error.
var text = Try(() =>
{
var path = Console.ReadLine()!;
return File.ReadAllText(path);
});
```

int x = result.Unwrap();
// or
int y = (int)result;
`Try` variants also exist for `Map` and `Then`, namely `TryMap` and `ThenTry`.

```cs
// Read console input and assert that it isn't null.
var path = Console.ReadLine().NotNull();

int z = result.Expect("Expected result to be successful.");
// Try to map the input by reading a file specified by the input.
// If ReadAllText throws an exception, the exception will be returned as an error.
var text = path.TryMap(p => File.ReadAllText(p));
```
1 change: 1 addition & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
_site/
2 changes: 2 additions & 0 deletions docs/api/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
.manifest
*.yml
9 changes: 9 additions & 0 deletions docs/api/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
See the table of contents for an overview of the library API.

<br/>

## Important types

- [`Result<T>`](/api/Rascal.Result-1.html)
- [`Prelude`](/api/Rascal.Prelude.html)
- [`Error`](/api/Rascal.Error.html)
1 change: 1 addition & 0 deletions docs/diagnostics/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
The main Rascal package has a built-in suite of analyzers and code fixes. This section describes the various diagnostics which may be produced by these analyzers.
31 changes: 31 additions & 0 deletions docs/diagnostics/rascal001.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Use 'Map' instead of 'Then(x => Ok(...))'

<br/>

<div class="text-secondary lh-lg" style="font-size: 14px;">
Id: RASCAL001
<br/>
Severity: <span class="text-warning">warning</span>
<br/>
Has code fix: <span class="text-success">yes</span>
<br/>
</div>

<br/>

## Description

*RASCAL001* is reported when [`Ok`](/api/Rascal.Prelude.html#Rascal_Prelude_Ok__1___0_) or any other form of result construction is used as the immediate return from the lambda inside a [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) call. Because [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) chains results, this is equivalent to a much simpler [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) call. The warning can be resolved by replacing the [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) call with a [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) which maps to the expression inside the [`Ok`](/api/Rascal.Prelude.html#Rascal_Prelude_Ok__1___0_).

<br/>

### Example

```cs
var a = Ok(2);

var b = a.Then(x => Ok(F(x))); // RASCAL001
// Fix:
var b = a.Map(x => F(x));
```
31 changes: 31 additions & 0 deletions docs/diagnostics/rascal002.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Use Then instead of 'Map(...).Unnest()'

<br/>

<div class="text-secondary lh-lg" style="font-size: 14px;">
Id: RASCAL002
<br/>
Severity: <span class="text-warning">warning</span>
<br/>
Has code fix: <span class="text-success">yes</span>
<br/>
</div>

<br/>

## Description

*RASCAL002* is reported when [`Unnest`](/api/Rascal.ResultExtensions.html#Rascal_ResultExtensions_Unnest__1_Rascal_Result_Rascal_Result___0___) is called immediately after a [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) call. This operation is equivalent to calling [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) with the same mapping function, which improves clarity and performance. The warning can be resolved by removing the [`Unnest`](/api/Rascal.ResultExtensions.html#Rascal_ResultExtensions_Unnest__1_Rascal_Result_Rascal_Result___0___) call and replacing the [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) call with a [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__).

<br/>

### Example

```cs
var a = Ok(2);

var b = a.Map(x => F(x)).Unnest(); // RASCAL002
// Fix:
var b = a.Then(x => F(x));
```
Loading

0 comments on commit 32f32d0

Please sign in to comment.