Skip to content

Commit

Permalink
Merge pull request #5 from thinker227/main
Browse files Browse the repository at this point in the history
1.1.0
  • Loading branch information
thinker227 authored Jan 15, 2024
2 parents 15b57cc + 3c8223c commit 81c84c2
Show file tree
Hide file tree
Showing 96 changed files with 3,939 additions and 537 deletions.
2 changes: 1 addition & 1 deletion .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ end_of_line = lf
insert_final_newline = true

# XML files
[*.{xml,csproj,nuspec}]
[*.{xml,csproj,props,nuspec}]
indent_size = 2

# YML files
Expand Down
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
30 changes: 30 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Release

on:
workflow_dispatch:
push:
branches:
- release

jobs:
release:

runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: Install .NET
uses: actions/setup-dotnet@v3
with:
dotnet-version: '8.0.x'
- name: Install dependencies
run: dotnet restore src/Rascal
- name: Build
run: dotnet build src/Rascal --no-restore
- name: Create package
run: dotnet pack -c Release --no-build
- name: Push to Nuget
run: dotnet nuget push package/*.nupkg --source "https://api.nuget.org/v3/index.json" --api-key ${{ secrets.NUGET_API_KEY }}
30 changes: 30 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<Project>

<!-- Build properties -->
<PropertyGroup>
<LangVersion>latest</LangVersion>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<WarningsAsErrors>nullable</WarningsAsErrors>
</PropertyGroup>

<!-- Package properties -->
<PropertyGroup>
<PackageOutputPath>../../package</PackageOutputPath>
</PropertyGroup>

<!-- Package metadata -->
<PropertyGroup>
<Authors>thinker227</Authors>
<Copyright>thinker227 2023</Copyright>
<PackageLicenseFile>LICENSE.txt</PackageLicenseFile>
<RepositoryUrl>https://github.com/thinker227/Rascal</RepositoryUrl>
<RepositoryType>git</RepositoryType>
</PropertyGroup>

<!-- Additional package files -->
<ItemGroup>
<None Include="../../LICENSE.txt" Pack="true" PackagePath="/" />
</ItemGroup>

</Project>
217 changes: 158 additions & 59 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,99 +1,198 @@
# Rascal
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#.
<div align="center">
<h1>Rascal</h1>
<img alt="Build and test status" src="https://img.shields.io/github/actions/workflow/status/thinker227/Rascal/build-test.yml?style=for-the-badge&label=Build%20%26%20Test">
<img alt="Nuget" src="https://img.shields.io/nuget/vpre/Rascal?style=for-the-badge&label=Nuget%3A%20Rascal">
</div>

<br></br>

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.

## The prelude
The full library documentation is available [here](https://thinker227.github.io/Rascal/).

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.
<br/>

## Samples
# Installation

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

// Through implicit conversion
Result<string> implicitOk = "owo";
Run in a terminal in the root of your project:

```ps1
dotnet add package Rascal --prerelease
```

### Mapping a result
</details>

<details>
<summary>Package manager console</summary>

Run from the Visual Studio Package Manager console:

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

</details>

<details>
<summary>Script environment</summary>

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"
```

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.
If you wish to install a specific version of the package, specify the package version:

```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"
```

</details>

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

static Result<int> DivSafe(int a, int b) =>
b != 0
? a / b
: "Cannot divide by 0.";
Add under an `ItemGroup` node in your project file:

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

The above expression for `val` can alternatively be written using query syntax:
Obviously, replace `1.0.1-pre` with the actual package version you want.

</details>

<br/>

# 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.

```cs
var val =
from a in num
from b in den
from x in DivSafe(a, b)
select x;
// 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");
```

### Various utilities
## 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`.

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()!);
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()!);

// 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));
```

Turn a nullable value into a result.
```cs
var result = F().NotNull();
<br/>

static int? F();
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
// 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));
});
```

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`.
<br/>

`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 result = Try(() => File.ReadAllText(path));
// 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));
```

Validate inputs directly inside a result expression chain, replacing the original value with an error if the predicate fails.
## Validation

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

```cs
var input = ParseR<int>(Console.ReadLine()!)
// 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));
});
```

### "Unsafe" operations
## Exception handling

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));
```
Loading

0 comments on commit 81c84c2

Please sign in to comment.