From 29fee9a4d4b2f3bd8f37664854e18fe2d4c3d7ea Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sun, 31 Dec 2023 01:35:25 +0100 Subject: [PATCH 01/21] Add docfx stuff --- docs/.gitignore | 2 + docs/docfx.json | 45 ++++++++++++++++++++ docs/docs/getting-started.md | 81 ++++++++++++++++++++++++++++++++++++ docs/docs/toc.yml | 5 +++ docs/docs/using-result.md | 63 ++++++++++++++++++++++++++++ docs/index.md | 24 +++++++++++ docs/toc.yml | 4 ++ 7 files changed, 224 insertions(+) create mode 100644 docs/.gitignore create mode 100644 docs/docfx.json create mode 100644 docs/docs/getting-started.md create mode 100644 docs/docs/toc.yml create mode 100644 docs/docs/using-result.md create mode 100644 docs/index.md create mode 100644 docs/toc.yml diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..d5bcab1 --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +_site/ +api/ diff --git a/docs/docfx.json b/docs/docfx.json new file mode 100644 index 0000000..26d120f --- /dev/null +++ b/docs/docfx.json @@ -0,0 +1,45 @@ +{ + "metadata": [ + { + "src": [ + { + "src": "../src", + "files": [ + "Rascal/Rascal.csproj" + ] + } + ], + "dest": "api" + } + ], + "build": { + "content": [ + { + "files": [ + "**/*.{md,yml}" + ], + "exclude": [ + "_site/**" + ] + } + ], + "resource": [ + { + "files": [ + "images/**" + ] + } + ], + "output": "_site", + "template": [ + "default", + "modern" + ], + "globalMetadata": { + "_appName": "Rascal Docs", + "_appTitle": "Rascal Docs", + "_enableSearch": true, + "pdf": false + } + } +} diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md new file mode 100644 index 0000000..22cddf0 --- /dev/null +++ b/docs/docs/getting-started.md @@ -0,0 +1,81 @@ +# Getting started + +A quick setup guide for using Rascal. + +## Installing the package + +In order to use Rascal, you should first install the [Rascal Nuget package](https://www.nuget.org/packages/Rascal). + +# [.NET CLI](#tab/cli) + +Run in a terminal in the root of your project: + +```ps1 +dotnet add package Rascal --prerelease +``` + +# [Package manager console](#tab/pm) + +Run from the Visual Studio Package Manager console: + +```ps1 +NuGet\Install-Package Rascal -IncludePrerelease +``` + +# [REPL environment](#tab/repl) + +In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [F# Interactive](https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/), enter: + +```cs +#r "nuget: Rascal" +``` + +If you wish to install a pre-release version of the package, specify the package version: + +```cs +#r "nuget: Rascal, 1.0.1-pre" +``` + +For the special flavor of C# script mode used by the [MODiX REPL](https://github.com/discord-csharp/CSharpRepl): + +```cs +#nuget Rascal +``` + +> [!NOTE] +> The MODiX REPL currently doesn't support referencing specific package versions and therefore does not support pre-release versions of the package. + +# [`PackageReference`](#tab/csproj) + +Add under an `ItemGroup` node in the `.csproj` file of your project: + +```xml + +``` + +--- + +## Using the package + +Once you've installed the package, import the `Rascal` namespace containing the import types for Rascal: + +```cs +using Rascal; +``` + +You can now access the [`Result`](/api/Rascal.Result-1.html) type and fail away to your heart's content. See [Using `Result`](/docs/using-result.html) for a more detailed introduction to the `Result` type. + +### Importing the prelude + +Rascal features a [`Prelude`](/api/Rascal.Prelude.html) class which contains various utility methods which are meant to be imported *statically*. Create a new file named `Usings.cs` and add the following: + +```cs +global using static Rascal.Prelude; +``` + +This imports `Prelude` *globally* and *statically* such that all its methods are available everywhere within your project without having to explicitly reference the `Prelude` class. + +```cs +var result = Ok("some value"); +var parsed = ParseR(Console.ReadLine()!); +``` diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml new file mode 100644 index 0000000..c325eb4 --- /dev/null +++ b/docs/docs/toc.yml @@ -0,0 +1,5 @@ +- name: Getting Started + href: getting-started.md + items: + - name: Using Result + href: using-result.md diff --git a/docs/docs/using-result.md b/docs/docs/using-result.md new file mode 100644 index 0000000..d1ef915 --- /dev/null +++ b/docs/docs/using-result.md @@ -0,0 +1,63 @@ +# Using `Result` + +## Introduction + +The most fundamental type Rascal provides is the [`Result`](/api/Rascal.Result-1.html) struct. `Result` can be summarized in a myriad of ways, but in the most simplest of terms, **`Result` represents either some value *or* an error**. This kind of value-or-error pattern (commonly and quite aptly referred to as the "result pattern") is applicable in a wide variety of situations, practically any time there is some kind of operation which might fail. In C# we typically use either the `Try*` pattern or exceptions to represent this, but the `Try*` pattern is somewhat verbose and ugly, and exceptions have performance penalties and are hard to catch and handle properly (and most importantly are usually not documented very well). The result pattern and the `Result` type aim to solve these issues by *1)* documenting the possibility of failure on the *type* level, and *2)* forcing you to handle the possibility of an error occuring. Neither the `Try*` pattern nor exceptions do either of these things very well. + +### An example + +So, how do you use `Result`? In the simplest case, imagine a method which takes the ID of a user and returns the user with that ID: + +```cs +public async Task GetUser(int userId) +{ + var user = await db.Users.FirstOrDefaultAsync(user => user.Id == userId); + + return user; +} +``` + +There are two things to note about this method: *1)* we're returning `User?` through the task, which doesn't communicate intent particularly well, and *2)* `FirstOrDefaultAsync` might throw an exception. Let's rewrite this to use `Result` with proper handling for both of these situations: + +```cs +public Task> GetUser(int userId) => TryAsync(async () => +{ + var user = await db.Users.FirstOrDefaultAsync(user => user.Id == userId); + + if (user is null) return Err(new NotFoundError($"Could not find a user with id {userId}.")); + + return user; +}); +``` + +Now, suddenly this code can handle `FirstOrDefaultAsync` throwing an exception, and the case where no user with the ID `userId` can be found. + +Of course, this is a somewhat contrived example of when using `Result` would be useful, but it gets the point across. + +### WHAT DO I DO WITH THIS??? + +So you have a `Result` instance now, great! Now what can you do with this?? As a good programmer, you look through the intellisense context menu on what this type has to offer, but fail to see anything resembling a `Value` property. This is because `Result` does not contain a `Value` property. Instead, if you're given a `Result`, you have two options on what to do with it: continue working with it, molding it like a lump of clay until the value inside it resembles what you want it to, or try *unwrapping* the value to see if it exists, then do whatever you want with that value. The former of these two options is usually the better and less cumbersome one, by not requiring you to constantly have to check whether the result actually contains anything useful, rather letting you happily pretend it does until reality comes crashing down and you realize it doesn't (or it actually does!). + +Following the two above paths to functional Nirvana, you can group the methods `Result` provides in two: [mapping](#mapping) and [unwrapping](#unwrapping). + +## Mapping + +*Mapping* in the context of `Result` is akin to reaching inside a shut box and doing something with the contents of that box. If the box contains something, when you finally open the box you'll see the fruits of your labor, otherwise if the box was empty from the start or you somehow managed to destory the thing inside while you were working with it, you'll just have an empty box in the end. This is in essence what mapping is. + +The first method you might find in `Result` on this topic is the aptly named [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) method, which full signature is + +```cs +public Result Map(Func mapping) +``` + +Remember `T` here is some kind of value. We don't actually know if that value exists, but `Map` will let us pretend it does. The one parameter the method takes is a function which takes a `T` and returns a `TNew`, which can be absolutely whatever we want. If we had a result returned by `ParseR` (a `Result`), we could perhaps try turn it into a string: + +```cs +Result result = ParseR(Console.ReadLine()!); + +Result newResult = result.Map(int value => value.ToString()); +``` + +This isn't particularly useful in a real-world case, but it demonstrates what `Map` does. Now if we imagine `result` was actually an error, `Map` wouldn't have a value to give us, so instead the error is just passed along to the next step. + +## Unwrapping diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..67ba445 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,24 @@ +--- +_layout: landing +--- + +# 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#. + +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. + +For an installation guide (it's just a Nuget package), see [Getting started](/docs/getting-started.html). + +For a comprehensive guide on using the library, see [Using `Result`](/docs/using-result.html). + +## Other great libraries + +Some libraries Rascal takes inspiration from: + +- [Rust's std::result](https://doc.rust-lang.org/std/result) +- [Remora.Results](https://github.com/Remora/Remora.Results) +- [Pidgin](https://github.com/benjamin-hodgson/Pidgin) +- [SuperLinq](https://github.com/viceroypenguin/SuperLinq) +- [HonkSharp](https://github.com/asc-community/HonkSharp) +- [error-or](https://github.com/amantinband/error-or) diff --git a/docs/toc.yml b/docs/toc.yml new file mode 100644 index 0000000..061acc6 --- /dev/null +++ b/docs/toc.yml @@ -0,0 +1,4 @@ +- name: Docs + href: docs/ +- name: API + href: api/ \ No newline at end of file From dddf1d9365dade99d0a2755f8040f45042752f7e Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sun, 31 Dec 2023 12:42:01 +0100 Subject: [PATCH 02/21] Update index.md --- docs/index.md | 134 ++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 131 insertions(+), 3 deletions(-) diff --git a/docs/index.md b/docs/index.md index 67ba445..5111c43 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,15 +8,143 @@ Rascal is a simple and lightweight [result type](https://www.youtube.com/watch?v 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. -For an installation guide (it's just a Nuget package), see [Getting started](/docs/getting-started.html). +## Installation -For a comprehensive guide on using the library, see [Using `Result`](/docs/using-result.html). +# [.NET CLI](#tab/cli) + +Run in a terminal in the root of your project: + +```ps1 +dotnet add package Rascal --prerelease +``` + +# [Package manager console](#tab/pm) + +Run from the Visual Studio Package Manager console: + +```ps1 +NuGet\Install-Package Rascal -IncludePrerelease +``` + +# [REPL environment](#tab/repl) + +In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [F# Interactive](https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/), enter: + +```cs +#r "nuget: Rascal" +``` + +If you wish to install a pre-release version of the package, specify the package version: + +```cs +#r "nuget: Rascal, 1.0.1-pre" +``` + +For the special flavor of C# script mode used by the [MODiX REPL](https://github.com/discord-csharp/CSharpRepl): + +```cs +#nuget Rascal +``` + +> [!NOTE] +> The MODiX REPL currently doesn't support referencing specific package versions and therefore does not support pre-release versions of the package. + +# [`PackageReference`](#tab/csproj) + +Add under an `ItemGroup` node in your project file: + +```xml + +``` + +Obviously, replace `1.0.1-pre` with the actual package version you want. + +--- + +## Quick start + +After installing the package, create a file called `Usings.cs` and add the following: + +```cs +global using static Rascal.Prelude; +``` + +[`Prelude`](/api/Rascal.Prelude.html) includes a variety of utility functions which you can now access from anywhere in your project. + +Now, let's pretend you have an ASP.NET Core app with the following method in a service: + +```cs +public Task GetUser(int userId) +{ + var user = db.Users.FirstOrDefaultAsync(user => user.Id == userId); + + return user; +} +``` + +... you can replace it with: + +```cs +public Task> GetUser(int userId) => TryAsync(() => +{ + var user = db.Users.FirstOrDefaultAsync(user => user.Id == userId); + + if (user is null) return new NotFoundError($"User with ID {userId} does not exist."); + + return user; +}); +``` + +This code will handle catching any exceptions thrown by `FirstOrDefaultAsync` and will return a `NotFoundError` if the user isn't found. Now you can use this method as such: + +# [Minimal API](#tab/minimal) + +```cs +app.MapGet("/users/{id}", async (int id, IUserService userService) => +{ + var userResult = await userService.GetUser(id); + + return userResult.Match( + user => Results.Ok(user), + error => error switch + { + NotFoundError => Results.NotFound(), + _ => Results.Problem(detail: error.Message, statusCode: 500) + } + ); +}); +``` + +# [Controller](#tab/controller) + +```cs +public sealed class UsersController(IUserService userService) : ControllerBase +{ + [HttpGet("/users/{id}")] + public async Task Get(int id) + { + var user = await userService.GetUser(id); + + return user.Match( + user => this.Ok(user), + IActionResult (error) => error switch + { + NotFoundError => this.NotFound(), + _ => this.Problem(detail: error.Message, statusCode: 500) + } + ); + } +} +``` + +--- ## Other great libraries Some libraries Rascal takes inspiration from: -- [Rust's std::result](https://doc.rust-lang.org/std/result) +- Rust's [std::result](https://doc.rust-lang.org/std/result) +- Haskell's [Data.Maybe](https://hackage.haskell.org/package/base-4.19.0.0/docs/Data-Maybe.html) and [Control.Monad](https://hackage.haskell.org/package/base-4.19.0.0/docs/Control-Monad.html) - [Remora.Results](https://github.com/Remora/Remora.Results) - [Pidgin](https://github.com/benjamin-hodgson/Pidgin) - [SuperLinq](https://github.com/viceroypenguin/SuperLinq) From 2ed9cb08909b43e5d6c8e57ffded3259b94cc98b Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sun, 31 Dec 2023 12:42:37 +0100 Subject: [PATCH 03/21] Remove docs tab --- docs/docs/getting-started.md | 81 ------------------------------------ docs/docs/toc.yml | 5 --- docs/docs/using-result.md | 63 ---------------------------- docs/toc.yml | 2 - 4 files changed, 151 deletions(-) delete mode 100644 docs/docs/getting-started.md delete mode 100644 docs/docs/toc.yml delete mode 100644 docs/docs/using-result.md diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md deleted file mode 100644 index 22cddf0..0000000 --- a/docs/docs/getting-started.md +++ /dev/null @@ -1,81 +0,0 @@ -# Getting started - -A quick setup guide for using Rascal. - -## Installing the package - -In order to use Rascal, you should first install the [Rascal Nuget package](https://www.nuget.org/packages/Rascal). - -# [.NET CLI](#tab/cli) - -Run in a terminal in the root of your project: - -```ps1 -dotnet add package Rascal --prerelease -``` - -# [Package manager console](#tab/pm) - -Run from the Visual Studio Package Manager console: - -```ps1 -NuGet\Install-Package Rascal -IncludePrerelease -``` - -# [REPL environment](#tab/repl) - -In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [F# Interactive](https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/), enter: - -```cs -#r "nuget: Rascal" -``` - -If you wish to install a pre-release version of the package, specify the package version: - -```cs -#r "nuget: Rascal, 1.0.1-pre" -``` - -For the special flavor of C# script mode used by the [MODiX REPL](https://github.com/discord-csharp/CSharpRepl): - -```cs -#nuget Rascal -``` - -> [!NOTE] -> The MODiX REPL currently doesn't support referencing specific package versions and therefore does not support pre-release versions of the package. - -# [`PackageReference`](#tab/csproj) - -Add under an `ItemGroup` node in the `.csproj` file of your project: - -```xml - -``` - ---- - -## Using the package - -Once you've installed the package, import the `Rascal` namespace containing the import types for Rascal: - -```cs -using Rascal; -``` - -You can now access the [`Result`](/api/Rascal.Result-1.html) type and fail away to your heart's content. See [Using `Result`](/docs/using-result.html) for a more detailed introduction to the `Result` type. - -### Importing the prelude - -Rascal features a [`Prelude`](/api/Rascal.Prelude.html) class which contains various utility methods which are meant to be imported *statically*. Create a new file named `Usings.cs` and add the following: - -```cs -global using static Rascal.Prelude; -``` - -This imports `Prelude` *globally* and *statically* such that all its methods are available everywhere within your project without having to explicitly reference the `Prelude` class. - -```cs -var result = Ok("some value"); -var parsed = ParseR(Console.ReadLine()!); -``` diff --git a/docs/docs/toc.yml b/docs/docs/toc.yml deleted file mode 100644 index c325eb4..0000000 --- a/docs/docs/toc.yml +++ /dev/null @@ -1,5 +0,0 @@ -- name: Getting Started - href: getting-started.md - items: - - name: Using Result - href: using-result.md diff --git a/docs/docs/using-result.md b/docs/docs/using-result.md deleted file mode 100644 index d1ef915..0000000 --- a/docs/docs/using-result.md +++ /dev/null @@ -1,63 +0,0 @@ -# Using `Result` - -## Introduction - -The most fundamental type Rascal provides is the [`Result`](/api/Rascal.Result-1.html) struct. `Result` can be summarized in a myriad of ways, but in the most simplest of terms, **`Result` represents either some value *or* an error**. This kind of value-or-error pattern (commonly and quite aptly referred to as the "result pattern") is applicable in a wide variety of situations, practically any time there is some kind of operation which might fail. In C# we typically use either the `Try*` pattern or exceptions to represent this, but the `Try*` pattern is somewhat verbose and ugly, and exceptions have performance penalties and are hard to catch and handle properly (and most importantly are usually not documented very well). The result pattern and the `Result` type aim to solve these issues by *1)* documenting the possibility of failure on the *type* level, and *2)* forcing you to handle the possibility of an error occuring. Neither the `Try*` pattern nor exceptions do either of these things very well. - -### An example - -So, how do you use `Result`? In the simplest case, imagine a method which takes the ID of a user and returns the user with that ID: - -```cs -public async Task GetUser(int userId) -{ - var user = await db.Users.FirstOrDefaultAsync(user => user.Id == userId); - - return user; -} -``` - -There are two things to note about this method: *1)* we're returning `User?` through the task, which doesn't communicate intent particularly well, and *2)* `FirstOrDefaultAsync` might throw an exception. Let's rewrite this to use `Result` with proper handling for both of these situations: - -```cs -public Task> GetUser(int userId) => TryAsync(async () => -{ - var user = await db.Users.FirstOrDefaultAsync(user => user.Id == userId); - - if (user is null) return Err(new NotFoundError($"Could not find a user with id {userId}.")); - - return user; -}); -``` - -Now, suddenly this code can handle `FirstOrDefaultAsync` throwing an exception, and the case where no user with the ID `userId` can be found. - -Of course, this is a somewhat contrived example of when using `Result` would be useful, but it gets the point across. - -### WHAT DO I DO WITH THIS??? - -So you have a `Result` instance now, great! Now what can you do with this?? As a good programmer, you look through the intellisense context menu on what this type has to offer, but fail to see anything resembling a `Value` property. This is because `Result` does not contain a `Value` property. Instead, if you're given a `Result`, you have two options on what to do with it: continue working with it, molding it like a lump of clay until the value inside it resembles what you want it to, or try *unwrapping* the value to see if it exists, then do whatever you want with that value. The former of these two options is usually the better and less cumbersome one, by not requiring you to constantly have to check whether the result actually contains anything useful, rather letting you happily pretend it does until reality comes crashing down and you realize it doesn't (or it actually does!). - -Following the two above paths to functional Nirvana, you can group the methods `Result` provides in two: [mapping](#mapping) and [unwrapping](#unwrapping). - -## Mapping - -*Mapping* in the context of `Result` is akin to reaching inside a shut box and doing something with the contents of that box. If the box contains something, when you finally open the box you'll see the fruits of your labor, otherwise if the box was empty from the start or you somehow managed to destory the thing inside while you were working with it, you'll just have an empty box in the end. This is in essence what mapping is. - -The first method you might find in `Result` on this topic is the aptly named [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) method, which full signature is - -```cs -public Result Map(Func mapping) -``` - -Remember `T` here is some kind of value. We don't actually know if that value exists, but `Map` will let us pretend it does. The one parameter the method takes is a function which takes a `T` and returns a `TNew`, which can be absolutely whatever we want. If we had a result returned by `ParseR` (a `Result`), we could perhaps try turn it into a string: - -```cs -Result result = ParseR(Console.ReadLine()!); - -Result newResult = result.Map(int value => value.ToString()); -``` - -This isn't particularly useful in a real-world case, but it demonstrates what `Map` does. Now if we imagine `result` was actually an error, `Map` wouldn't have a value to give us, so instead the error is just passed along to the next step. - -## Unwrapping diff --git a/docs/toc.yml b/docs/toc.yml index 061acc6..a2a2fa2 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,4 +1,2 @@ -- name: Docs - href: docs/ - name: API href: api/ \ No newline at end of file From 876196ae5c4f554377219d535742a6e6c2eac777 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sun, 31 Dec 2023 12:45:17 +0100 Subject: [PATCH 04/21] Update API stuff --- docs/.gitignore | 1 - docs/api/.gitignore | 2 ++ docs/api/index.md | 9 +++++++++ docs/toc.yml | 7 ++++++- 4 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 docs/api/.gitignore create mode 100644 docs/api/index.md diff --git a/docs/.gitignore b/docs/.gitignore index d5bcab1..57510a2 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,2 +1 @@ _site/ -api/ diff --git a/docs/api/.gitignore b/docs/api/.gitignore new file mode 100644 index 0000000..8f5a0e4 --- /dev/null +++ b/docs/api/.gitignore @@ -0,0 +1,2 @@ +.manifest +*.yml diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 0000000..6c77e4e --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,9 @@ +See the table of contents for an overview of the library API. + +
+ +## Important types + +- [`Result`](/api/Rascal.Result-1.html) +- [`Prelude`](/api/Rascal.Prelude.html) +- [`Error`](/api/Rascal.Error.html) diff --git a/docs/toc.yml b/docs/toc.yml index a2a2fa2..476ce5b 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,2 +1,7 @@ - name: API - href: api/ \ No newline at end of file + href: api/ + homepage: api/index.md +- name: Github + href: https://github.com/thinker227/Rascal +- name: Nuget + href: https://www.nuget.org/packages/Rascal From b68eabacbef1afadcd883354ced44ddb21ee7d78 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sun, 31 Dec 2023 13:12:35 +0100 Subject: [PATCH 05/21] Add some spaceee --- docs/index.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/index.md b/docs/index.md index 5111c43..d016279 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,6 +8,8 @@ Rascal is a simple and lightweight [result type](https://www.youtube.com/watch?v 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. +
+ ## Installation # [.NET CLI](#tab/cli) @@ -61,6 +63,8 @@ Obviously, replace `1.0.1-pre` with the actual package version you want. --- +
+ ## Quick start After installing the package, create a file called `Usings.cs` and add the following: @@ -139,6 +143,8 @@ public sealed class UsersController(IUserService userService) : ControllerBase --- +
+ ## Other great libraries Some libraries Rascal takes inspiration from: From 669761256519e4542cac464fa196aca52c49109c Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sun, 31 Dec 2023 16:10:32 +0100 Subject: [PATCH 06/21] Add some links --- docs/index.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/index.md b/docs/index.md index d016279..4108266 100644 --- a/docs/index.md +++ b/docs/index.md @@ -99,7 +99,7 @@ public Task> GetUser(int userId) => TryAsync(() => }); ``` -This code will handle catching any exceptions thrown by `FirstOrDefaultAsync` and will return a `NotFoundError` if the user isn't found. Now you can use this method as such: +This code will handle catching any exceptions thrown by [`FirstOrDefaultAsync`](https://learn.microsoft.com/en-us/dotnet/api/system.data.entity.queryableextensions.firstordefaultasync) and will return a [`NotFoundError`](/api/Rascal.Errors.NotFoundError.html) if the user isn't found. Now you can use this method as such: # [Minimal API](#tab/minimal) From 2054e5f679ef92654e91c960e6317e7593502c3f Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sun, 31 Dec 2023 16:10:38 +0100 Subject: [PATCH 07/21] Remove search --- docs/docfx.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docfx.json b/docs/docfx.json index 26d120f..c3c142f 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -38,7 +38,7 @@ "globalMetadata": { "_appName": "Rascal Docs", "_appTitle": "Rascal Docs", - "_enableSearch": true, + "_enableSearch": false, "pdf": false } } From 9c2068d20c0837e5101cd9515e2594d7e7fc8941 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Tue, 2 Jan 2024 10:23:27 +0100 Subject: [PATCH 08/21] Add overwrite files --- docs/docfx.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/docfx.json b/docs/docfx.json index c3c142f..54cee53 100644 --- a/docs/docfx.json +++ b/docs/docfx.json @@ -19,6 +19,7 @@ "**/*.{md,yml}" ], "exclude": [ + "apidocs/**.md", "_site/**" ] } @@ -30,6 +31,13 @@ ] } ], + "overwrite": [ + { + "files": [ + "apidocs/**.md" + ] + } + ], "output": "_site", "template": [ "default", From 9cf9d227d889ffca502651dbf714eeff9a740bae Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 5 Jan 2024 22:26:19 +0100 Subject: [PATCH 09/21] Update index.md --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index 4108266..2450b01 100644 --- a/docs/index.md +++ b/docs/index.md @@ -28,9 +28,9 @@ Run from the Visual Studio Package Manager console: NuGet\Install-Package Rascal -IncludePrerelease ``` -# [REPL environment](#tab/repl) +# [Script environment](#tab/repl) -In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [F# Interactive](https://learn.microsoft.com/en-us/dotnet/fsharp/tools/fsharp-interactive/), enter: +In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [RoslynPad](https://roslynpad.net), enter: ```cs #r "nuget: Rascal" From b733d049e60aa1deb1a9c86868f929f3f47a382d Mon Sep 17 00:00:00 2001 From: thinker227 Date: Sun, 7 Jan 2024 00:48:52 +0100 Subject: [PATCH 10/21] Update index.md --- docs/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/index.md b/docs/index.md index 2450b01..798d8ac 100644 --- a/docs/index.md +++ b/docs/index.md @@ -30,13 +30,13 @@ NuGet\Install-Package Rascal -IncludePrerelease # [Script environment](#tab/repl) -In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [RoslynPad](https://roslynpad.net), enter: +In environments such as [dotnet-script](https://github.com/dotnet-script/dotnet-script), [C# REPL](https://github.com/waf/CSharpRepl), or [RoslynPad](https://roslynpad.net), enter: ```cs #r "nuget: Rascal" ``` -If you wish to install a pre-release version of the package, specify the package version: +If you wish to install a specific version of the package, specify the package version: ```cs #r "nuget: Rascal, 1.0.1-pre" @@ -89,9 +89,9 @@ public Task GetUser(int userId) ... you can replace it with: ```cs -public Task> GetUser(int userId) => TryAsync(() => +public Task> GetUser(int userId) => TryAsync(async () => { - var user = db.Users.FirstOrDefaultAsync(user => user.Id == userId); + var user = await db.Users.FirstOrDefaultAsync(user => user.Id == userId); if (user is null) return new NotFoundError($"User with ID {userId} does not exist."); From 47d1d79187acf94cdf92a6e4add12c8293e62be6 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Wed, 10 Jan 2024 14:35:34 +0100 Subject: [PATCH 11/21] Add Parse extension variants --- src/Rascal/ResultExtensions.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Rascal/ResultExtensions.cs b/src/Rascal/ResultExtensions.cs index d83c3ea..cb23a2e 100644 --- a/src/Rascal/ResultExtensions.cs +++ b/src/Rascal/ResultExtensions.cs @@ -207,4 +207,23 @@ public static async Task> CatchCancellation(this ValueTask task, } } #endif + +#if NET7_0_OR_GREATER // Support for generic math + + // These are just extension variants of their corresponding Prelude methods + // since extension methods in statically imported classes as not accessible. + + /// + [Pure] + public static Result Parse(this string? s, IFormatProvider? provider = null, Error? error = null) + where T : IParsable => + Prelude.ParseR(s, provider, error); + + /// + [Pure] + public static Result Parse(this ReadOnlySpan s, IFormatProvider? provider = null, Error? error = null) + where T : ISpanParsable => + Prelude.ParseR(s, provider, error); + +#endif } From 9a6d74cf511eecc869631f81ae63ca7531214b57 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Wed, 10 Jan 2024 15:24:39 +0100 Subject: [PATCH 12/21] Add diagnostics docs --- docs/diagnostics/index.md | 1 + docs/diagnostics/rascal001.md | 27 ++++++++++++++++++++++++++ docs/diagnostics/rascal002.md | 27 ++++++++++++++++++++++++++ docs/diagnostics/rascal003.md | 27 ++++++++++++++++++++++++++ docs/diagnostics/rascal004.md | 21 ++++++++++++++++++++ docs/diagnostics/rascal005.md | 21 ++++++++++++++++++++ docs/diagnostics/rascal006.md | 36 +++++++++++++++++++++++++++++++++++ docs/diagnostics/rascal007.md | 17 +++++++++++++++++ docs/diagnostics/toc.yml | 14 ++++++++++++++ docs/toc.yml | 3 +++ 10 files changed, 194 insertions(+) create mode 100644 docs/diagnostics/index.md create mode 100644 docs/diagnostics/rascal001.md create mode 100644 docs/diagnostics/rascal002.md create mode 100644 docs/diagnostics/rascal003.md create mode 100644 docs/diagnostics/rascal004.md create mode 100644 docs/diagnostics/rascal005.md create mode 100644 docs/diagnostics/rascal006.md create mode 100644 docs/diagnostics/rascal007.md create mode 100644 docs/diagnostics/toc.yml diff --git a/docs/diagnostics/index.md b/docs/diagnostics/index.md new file mode 100644 index 0000000..e797424 --- /dev/null +++ b/docs/diagnostics/index.md @@ -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. diff --git a/docs/diagnostics/rascal001.md b/docs/diagnostics/rascal001.md new file mode 100644 index 0000000..4ff8ff3 --- /dev/null +++ b/docs/diagnostics/rascal001.md @@ -0,0 +1,27 @@ +# Use 'Map' instead of 'Then(x => Ok(...))' + +Id: *RASCAL001* + +Severity: *warning* + +> [!TIP] +> This diagnostic has an associated automatic code fix. + +
+ +## Description + +Calling '[Ok](/api/Rascal.Prelude.html#Rascal_Prelude_Ok__1___0_)' directly inside a '[Then](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___)' call is equivalent to calling '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)'. Use '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)' instead for clarity and performance. + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.Then(x => Ok(F(x))); // RASCAL001 + +// Fix: +var b = a.Then(x => Ok(F(x))); +``` diff --git a/docs/diagnostics/rascal002.md b/docs/diagnostics/rascal002.md new file mode 100644 index 0000000..66ba94f --- /dev/null +++ b/docs/diagnostics/rascal002.md @@ -0,0 +1,27 @@ +# Use Then instead of 'Map(...).Unnest()' + +Id: *RASCAL002* + +Severity: *warning* + +> [!TIP] +> This diagnostic has an associated automatic code fix. + +
+ +## Description + +Calling '[Unnest](/api/Rascal.ResultExtensions.html#Rascal_ResultExtensions_Unnest__1_Rascal_Result_Rascal_Result___0___)' directly after a '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)' call is equivalent to calling '[Then](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___)'. Use '[Then](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___)' instead for clarity and performance. + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.Map(x => F(x)).Unnest(); // RASCAL002 + +// Fix: +var b = a.Then(x => F(x)); +``` diff --git a/docs/diagnostics/rascal003.md b/docs/diagnostics/rascal003.md new file mode 100644 index 0000000..c6fee7d --- /dev/null +++ b/docs/diagnostics/rascal003.md @@ -0,0 +1,27 @@ +# Unnecessary 'Map' call with identity function + +Id: *RASCAL003* + +Severity: *warning* + +> [!TIP] +> This diagnostic has an associated automatic code fix. + +
+ +## Description + +Calling '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)' with an identity function returns the same result as the input. Remove this call to '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)'. + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.Map(x => x); // RASCAL003 + +// Fix: +var b = a; +``` diff --git a/docs/diagnostics/rascal004.md b/docs/diagnostics/rascal004.md new file mode 100644 index 0000000..542cb0d --- /dev/null +++ b/docs/diagnostics/rascal004.md @@ -0,0 +1,21 @@ +# 'To' called with same type as result + +Id: *RASCAL004* + +Severity: *warning* + +
+ +## Description + +Calling '[To](/api/Rascal.Result-1.html#Rascal_Result_1_To__1_Rascal_Error_)' with the same type as that of the result will always succeed. + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.To(); // RASCAL004 +``` diff --git a/docs/diagnostics/rascal005.md b/docs/diagnostics/rascal005.md new file mode 100644 index 0000000..3208e45 --- /dev/null +++ b/docs/diagnostics/rascal005.md @@ -0,0 +1,21 @@ +# 'To' called with impossible type + +Id: *RASCAL005* + +Severity: *warning* + +
+ +## Description + +Calling '[To](/api/Rascal.Result-1.html#Rascal_Result_1_To__1_Rascal_Error_)' with a type which no value of the type of the result permits will always fail. + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.To(); // RASCAL005 +``` diff --git a/docs/diagnostics/rascal006.md b/docs/diagnostics/rascal006.md new file mode 100644 index 0000000..11111f2 --- /dev/null +++ b/docs/diagnostics/rascal006.md @@ -0,0 +1,36 @@ +# Use 'GetValueOr' instead of 'Match(x => x, ...)' + +Id: *RASCAL006* + +Severity: *warning* + +> [!TIP] +> This diagnostic has an associated automatic code fix. + +
+ +## Description + +Calling '[Match](/api/Rascal.Result-1.html#Rascal_Result_1_Match__1_System_Func__0___0__System_Func_Rascal_Error___0__)' with an identity function for the 'ifOk' parameter is equivalent to '[DefaultOr](/api/Rascal.Result-1.html#Rascal_Result_1_GetValueOr_System_Func_Rascal_Error__0__)'. + +
+ +### Example + +```cs +var a = Ok(2); + +var b = a.Match(x => x, e => 0); // RASCAL006 + +// Fix: +var b = a.DefaultOr(0); +``` + +```cs +var a = Ok(2); + +var b = a.Match(x => x, e => F(e)); // RASCAL006 + +// Fix: +var b = a.DefaultOr(e => F(e)); +``` diff --git a/docs/diagnostics/rascal007.md b/docs/diagnostics/rascal007.md new file mode 100644 index 0000000..9912301 --- /dev/null +++ b/docs/diagnostics/rascal007.md @@ -0,0 +1,17 @@ +# Missing symbol required for analysis + +Id: *RASCAL007* + +Severity: *warning* + +
+ +## Description + +Cannot find type or member '*member*' which is required for analysis. No analysis will be performed. Verify that the version of the analyzer package matches that of the library, or report this as a bug. + +
+ +### Cause + +This diagnostic will only occur if the analyzer assembly is referenced without the main Rascal assembly being present. This is most probably the result of a bug. diff --git a/docs/diagnostics/toc.yml b/docs/diagnostics/toc.yml new file mode 100644 index 0000000..24af9c8 --- /dev/null +++ b/docs/diagnostics/toc.yml @@ -0,0 +1,14 @@ +- name: RASCAL001 + href: rascal001.md +- name: RASCAL002 + href: rascal002.md +- name: RASCAL003 + href: rascal003.md +- name: RASCAL004 + href: rascal004.md +- name: RASCAL005 + href: rascal005.md +- name: RASCAL006 + href: rascal006.md +- name: RASCAL007 + href: rascal007.md diff --git a/docs/toc.yml b/docs/toc.yml index 476ce5b..7385472 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,6 +1,9 @@ - name: API href: api/ homepage: api/index.md +- name: Diagnostics + href: diagnostics/ + homepage: diagnostics/index.md - name: Github href: https://github.com/thinker227/Rascal - name: Nuget From 81009e8a963f76b63f22b63dae2cc6fd41be82ed Mon Sep 17 00:00:00 2001 From: thinker227 Date: Wed, 10 Jan 2024 19:38:12 +0100 Subject: [PATCH 13/21] Update diagnostic docs --- docs/diagnostics/rascal001.md | 4 ++-- docs/diagnostics/rascal002.md | 2 +- docs/diagnostics/rascal003.md | 2 +- docs/diagnostics/rascal004.md | 8 ++++--- docs/diagnostics/rascal005.md | 20 +++++++++++++++--- docs/diagnostics/rascal006.md | 2 +- docs/diagnostics/rascal007.md | 9 +++----- src/Rascal.Analysis/Diagnostics.cs | 34 ++++++++++++++++++------------ 8 files changed, 50 insertions(+), 31 deletions(-) diff --git a/docs/diagnostics/rascal001.md b/docs/diagnostics/rascal001.md index 4ff8ff3..e2a4598 100644 --- a/docs/diagnostics/rascal001.md +++ b/docs/diagnostics/rascal001.md @@ -11,7 +11,7 @@ Severity: *warning* ## Description -Calling '[Ok](/api/Rascal.Prelude.html#Rascal_Prelude_Ok__1___0_)' directly inside a '[Then](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___)' call is equivalent to calling '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)'. Use '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)' instead for clarity and performance. +*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_).
@@ -23,5 +23,5 @@ var a = Ok(2); var b = a.Then(x => Ok(F(x))); // RASCAL001 // Fix: -var b = a.Then(x => Ok(F(x))); +var b = a.Map(x => F(x)); ``` diff --git a/docs/diagnostics/rascal002.md b/docs/diagnostics/rascal002.md index 66ba94f..13fe139 100644 --- a/docs/diagnostics/rascal002.md +++ b/docs/diagnostics/rascal002.md @@ -11,7 +11,7 @@ Severity: *warning* ## Description -Calling '[Unnest](/api/Rascal.ResultExtensions.html#Rascal_ResultExtensions_Unnest__1_Rascal_Result_Rascal_Result___0___)' directly after a '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)' call is equivalent to calling '[Then](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___)'. Use '[Then](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___)' instead for clarity and performance. +*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__).
diff --git a/docs/diagnostics/rascal003.md b/docs/diagnostics/rascal003.md index c6fee7d..0e21d89 100644 --- a/docs/diagnostics/rascal003.md +++ b/docs/diagnostics/rascal003.md @@ -11,7 +11,7 @@ Severity: *warning* ## Description -Calling '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)' with an identity function returns the same result as the input. Remove this call to '[Map](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__)'. +*RASCAL003* is reported when [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) is used with an *identity function*, i.e. a lambda in the form `x => x`. Because [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) transforms the ok value of a result, applying an identity function onto the value does nothing and returns the same result as the input, and the call is completely useless. The warning can be resolved by removing the call altogether.
diff --git a/docs/diagnostics/rascal004.md b/docs/diagnostics/rascal004.md index 542cb0d..3e56412 100644 --- a/docs/diagnostics/rascal004.md +++ b/docs/diagnostics/rascal004.md @@ -8,14 +8,16 @@ Severity: *warning* ## Description -Calling '[To](/api/Rascal.Result-1.html#Rascal_Result_1_To__1_Rascal_Error_)' with the same type as that of the result will always succeed. +*RASCAL004* is reported when [`To`](/api/Rascal.Result-1.html#Rascal_Result_1_To__1_Rascal_Error_) is called with the same type as that of the result it is called on. Because `.To()` is equivalent to an `is T` pattern, calling `.To` on a `Result` where both `T`s are the same type will always succeed if the result is ok, and the call is redundant. The warning can be resolved by removing the call altogether.
### Example ```cs -var a = Ok(2); +// Types added for clarity -var b = a.To(); // RASCAL004 +Result a = Ok(2); + +Result b = a.To(); // RASCAL004 ``` diff --git a/docs/diagnostics/rascal005.md b/docs/diagnostics/rascal005.md index 3208e45..3e4d72e 100644 --- a/docs/diagnostics/rascal005.md +++ b/docs/diagnostics/rascal005.md @@ -8,14 +8,28 @@ Severity: *warning* ## Description -Calling '[To](/api/Rascal.Result-1.html#Rascal_Result_1_To__1_Rascal_Error_)' with a type which no value of the type of the result permits will always fail. +*RASCAL005* is reported when [`To`](/api/Rascal.Result-1.html#Rascal_Result_1_To__1_Rascal_Error_) is called with a type which is incompatible with the type of the result. This mainly applies to class types which do not inherit each other, and structs which aren't the same type. Interfaces, type parameters, and `object` may always succeed regardless of which type they are casted to/from.
### Example ```cs -var a = Ok(2); +// Types added for clarity -var b = a.To(); // RASCAL005 +Result a = Ok(2); + +Result b = a.To(); // RASCAL005 +``` + +```cs +// Types added for clarity + +Result a = Ok(new B()); + +Result b = a.To(); // RASCAL005 + +class A; +class B : A; +class C : A; ``` diff --git a/docs/diagnostics/rascal006.md b/docs/diagnostics/rascal006.md index 11111f2..6f439f5 100644 --- a/docs/diagnostics/rascal006.md +++ b/docs/diagnostics/rascal006.md @@ -11,7 +11,7 @@ Severity: *warning* ## Description -Calling '[Match](/api/Rascal.Result-1.html#Rascal_Result_1_Match__1_System_Func__0___0__System_Func_Rascal_Error___0__)' with an identity function for the 'ifOk' parameter is equivalent to '[DefaultOr](/api/Rascal.Result-1.html#Rascal_Result_1_GetValueOr_System_Func_Rascal_Error__0__)'. +*RASCAL006* is reported when [`Match`](/api/Rascal.Result-1.html#Rascal_Result_1_Match__1_System_Func__0___0__System_Func_Rascal_Error___0__) is called with an *identity function* as its first argument, i.e. a lambda with the form `x => x`. Such a call is equivalent to [`GetValueOr`](/api/Rascal.Result-1.html#Rascal_Result_1_GetValueOr_System_Func_Rascal_Error__0__), with the remaining argument being a function which returns a value based on the result's error. Note that [`GetValueOr`](/api/Rascal.Result-1.html#Rascal_Result_1_GetValueOr_System_Func_Rascal_Error__0__) has three different overloads which are suitable depending on whether the result's error is needed when retrieving the value. The warning can be resolved by removing the first argument and replacing the call to [`Match`](/api/Rascal.Result-1.html#Rascal_Result_1_Match__1_System_Func__0___0__System_Func_Rascal_Error___0__) with [`GetValueOr`](/api/Rascal.Result-1.html#Rascal_Result_1_GetValueOr_System_Func_Rascal_Error__0__).
diff --git a/docs/diagnostics/rascal007.md b/docs/diagnostics/rascal007.md index 9912301..f020bf3 100644 --- a/docs/diagnostics/rascal007.md +++ b/docs/diagnostics/rascal007.md @@ -8,10 +8,7 @@ Severity: *warning* ## Description -Cannot find type or member '*member*' which is required for analysis. No analysis will be performed. Verify that the version of the analyzer package matches that of the library, or report this as a bug. +*RASCAL007* is reported if any type or member which is required by the analysis suite to perform analysis is found to be missing. The most likely cause of this is referencing the analysis assembly without referencing the core Rascal assembly, or referencing a higher version of the analysis assembly than that of the core assembly. This may also be the result of a bug in the analysis suite. -
- -### Cause - -This diagnostic will only occur if the analyzer assembly is referenced without the main Rascal assembly being present. This is most probably the result of a bug. +> [!IMPORTANT] +> If *RASCAL007* occurs, all analyzers from the analysis suite will completely stop working until the warning is resolved. diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index b01d84c..d7415ac 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -2,11 +2,14 @@ namespace Rascal.Analysis; public static class Diagnostics { + private const string CorrectnessCategory = "Correctness"; + private const string AnalysisCategory = "Analysis"; + public static DiagnosticDescriptor UseMap { get; } = new( "RASCAL001", "Use 'Map' instead of 'Then(x => Ok(...))'", - "Use 'Map' instead of calling 'Ok' directly inside 'Then'", - "Correctness", + "Use 'Map' instead of calling 'Ok' directly inside 'Then'.", + CorrectnessCategory, DiagnosticSeverity.Warning, true, "Calling 'Ok' directly inside a 'Then' call is equivalent to calling 'Map'. " + @@ -14,9 +17,9 @@ public static class Diagnostics public static DiagnosticDescriptor UseThen { get; } = new( "RASCAL002", - "Use Then instead of 'Map(...).Unnest()'", - "Use 'Then' instead of calling 'Unnest' directly after 'Map'", - "Correctness", + "Use 'Then' instead of 'Map(...).Unnest()'", + "Use 'Then' instead of calling 'Unnest' directly after 'Map'.", + CorrectnessCategory, DiagnosticSeverity.Warning, true, "Calling 'Unnest' directly after a 'Map' call is equivalent to calling 'Then'. " + @@ -26,8 +29,8 @@ public static class Diagnostics "RASCAL003", "Unnecessary 'Map' call with identity function", "This call maps {0} to itself. " + - "The call can be safely removed because it doesn't do anything", - "Correctness", + "The call can be safely removed because it doesn't do anything.", + CorrectnessCategory, DiagnosticSeverity.Warning, true, "Calling 'Map' with an identity function returns the same result as the input. " + @@ -38,17 +41,18 @@ public static class Diagnostics "'To' called with same type as result", "This call converts '{0}' to itself and will always succeed. " + "Remove this call to 'To' as it doesn't do anything.", - "Correctness", + CorrectnessCategory, DiagnosticSeverity.Warning, true, - "Calling 'To' with the same type as that of the result will always succeed."); + "Calling 'To' with the same type as that of the result will always succeed. " + + "Remove the call as it doesn't do anything."); public static DiagnosticDescriptor ToImpossibleType { get; } = new( "RASCAL005", "'To' called with impossible type", "This call tries to convert '{0}' to '{1}', but no value of type '{0}' can be of type '{1}'. " + "The conversion will always fail.", - "Correctness", + CorrectnessCategory, DiagnosticSeverity.Warning, true, "Calling 'To' with a type which no value of the type of the result permits will always fail."); @@ -58,18 +62,20 @@ public static class Diagnostics "Use 'GetValueOr' instead of 'Match(x => x, ...)'", "This call matches {0} using an identity function. " + "Use 'GetValueOr' instead to reduce allocations.", - "Correctness", + CorrectnessCategory, DiagnosticSeverity.Warning, true, - "Calling 'Match' with an identity function for the 'ifOk' parameter is equivalent to 'DefaultOr'."); + "Calling 'Match' with an identity function for the 'ifOk' parameter is equivalent to calling 'GetValueOr'. " + + "Replace this call with 'GetValueOr'."); public static DiagnosticDescriptor MissingSymbol { get; } = new( "RASCAL007", "Missing symbol required for analysis", "Cannot find type or member '{0}' which is required for analysis. " + "No analysis will be performed. " + - "Verify that the version of the analyzer package matches that of the library, or report this as a bug.", - "Analysis", + "Verify that the library is referenced and that the version of the analyzer assembly matches that of the library. " + + "Alternatively, this may be a bug and should be reported as such.", + AnalysisCategory, DiagnosticSeverity.Warning, true); } From cc12d1ec3be4fc70d7a2019468fb3d46d94c860e Mon Sep 17 00:00:00 2001 From: thinker227 Date: Wed, 10 Jan 2024 19:45:09 +0100 Subject: [PATCH 14/21] Update index.md --- docs/index.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/index.md b/docs/index.md index 798d8ac..689eb25 100644 --- a/docs/index.md +++ b/docs/index.md @@ -8,6 +8,8 @@ Rascal is a simple and lightweight [result type](https://www.youtube.com/watch?v 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. +Additionally, Rascal comes with a suite of analyzers and code fixes to help you write better and more reliable code using the library. The documentation for these analyzers can be found in the [diagnostics documentation](/diagnostics/index.html). +
## Installation @@ -145,6 +147,12 @@ public sealed class UsersController(IUserService userService) : ControllerBase
+### Explore the API + +Once you're ready to dive into the library, feel free to refer to the [API documentation](/api/index.html) for an in-depth look into each of the methods provided by the library. You can of course also explore the API through intellisense in your IDE of choice. + +
+ ## Other great libraries Some libraries Rascal takes inspiration from: From 385d39963eb84e96cf54af902474e12bc473697d Mon Sep 17 00:00:00 2001 From: thinker227 Date: Wed, 10 Jan 2024 19:55:24 +0100 Subject: [PATCH 15/21] Update readme(s) --- README.md | 113 ++------------------------------------------------ docs/index.md | 2 +- 2 files changed, 4 insertions(+), 111 deletions(-) diff --git a/README.md b/README.md index e239d90..de7bd5c 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,12 @@

-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. +
# Installation @@ -17,112 +19,3 @@ Rascal is first and foremost an aggregate of the result types I personally find # .NET CLI dotnet add package Rascal --prerelease ``` - -# Using the package - -## The prelude - -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. - -## Samples - -### Creating a result -```cs -// Through explicit Ok/Error functions -var explicitOk = Ok("uwu"); -var explicitErr = Err("An error occured."); - -// Through implicit conversion -Result implicitOk = "owo"; -``` - -### Mapping a result - -"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(Console.ReadLine()!) - .Map(x => Enumerable.Repeat("hewwo", x)); -``` - -
- -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`, 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(Console.ReadLine()!); -var den = ParseR(Console.ReadLine()!); - -var val = num.Then(a => den - .Map(b => DiveSafe(a, b))); - -static Result DivSafe(int a, int b) => - b != 0 - ? a / b - : "Cannot divide by 0."; -``` - -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; -``` - -
- -### Various utilities - -Parse a string or `ReadOnlySpan` to another type, returning a result. `ParseR` (short for `ParseResult`) works for any type implementing `IParsable` or `ISpanParsable`. -```cs -var parsed = ParseR(Console.ReadLine()!); -``` - -
- -Turn a nullable value into a result. -```cs -var result = F().NotNull(); - -static int? F(); -``` - -
- -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`. -```cs -var result = Try(() => File.ReadAllText(path)); -``` - -
- -Validate inputs directly inside a result expression chain, replacing the original value with an error if the predicate fails. -```cs -var input = ParseR(Console.ReadLine()!) - .Validate( - x => x >= 0, - x => $"Input {x} is less than 0.") - -// can also be written as -var input = - from x in ParseR(Console.ReadLine()!) - where x >= 0 - select x; -``` - -
- -### "Unsafe" operations - -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 result; - -int x = result.Unwrap(); -// or -int y = (int)result; - -int z = result.Expect("Expected result to be successful."); -``` diff --git a/docs/index.md b/docs/index.md index 689eb25..7674b55 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ _layout: landing # 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#. +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. From 7218a17d11223db0b3770d52a745e747a0b3f0ba Mon Sep 17 00:00:00 2001 From: thinker227 Date: Wed, 10 Jan 2024 19:55:39 +0100 Subject: [PATCH 16/21] Add package readme --- src/Rascal/Rascal.csproj | 4 ++-- src/Rascal/package-readme.md | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 src/Rascal/package-readme.md diff --git a/src/Rascal/Rascal.csproj b/src/Rascal/Rascal.csproj index 6293443..c40e86e 100644 --- a/src/Rascal/Rascal.csproj +++ b/src/Rascal/Rascal.csproj @@ -24,7 +24,7 @@ thinker227 thinker227 2023 LICENSE.txt - README.md + package-readme.md true https://github.com/thinker227/Rascal git @@ -32,7 +32,7 @@ - + diff --git a/src/Rascal/package-readme.md b/src/Rascal/package-readme.md new file mode 100644 index 0000000..7badc63 --- /dev/null +++ b/src/Rascal/package-readme.md @@ -0,0 +1,5 @@ +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. From 7b66a328066e6bdd3bc67f6760162e0d1818e29e Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 12 Jan 2024 17:57:29 +0100 Subject: [PATCH 17/21] Add samples --- docs/index.md | 17 ++++------ docs/samples/index.md | 64 +++++++++++++++++++++++++++++++++++++ docs/samples/toc.yml | 1 + docs/toc.yml | 3 ++ samples/.vscode/launch.json | 17 ++++++++++ samples/Combine.csx | 19 +++++++++++ samples/Construction.csx | 15 +++++++++ samples/Map.csx | 18 +++++++++++ samples/Then.csx | 23 +++++++++++++ samples/Try.csx | 14 ++++++++ samples/TryMap.csx | 13 ++++++++ samples/Validation.csx | 28 ++++++++++++++++ samples/omnisharp.json | 6 ++++ 13 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 docs/samples/index.md create mode 100644 docs/samples/toc.yml create mode 100644 samples/.vscode/launch.json create mode 100644 samples/Combine.csx create mode 100644 samples/Construction.csx create mode 100644 samples/Map.csx create mode 100644 samples/Then.csx create mode 100644 samples/Try.csx create mode 100644 samples/TryMap.csx create mode 100644 samples/Validation.csx create mode 100644 samples/omnisharp.json diff --git a/docs/index.md b/docs/index.md index 7674b55..a61659b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -32,7 +32,7 @@ NuGet\Install-Package Rascal -IncludePrerelease # [Script environment](#tab/repl) -In environments such as [dotnet-script](https://github.com/dotnet-script/dotnet-script), [C# REPL](https://github.com/waf/CSharpRepl), or [RoslynPad](https://roslynpad.net), enter: +In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [RoslynPad](https://roslynpad.net), enter: ```cs #r "nuget: Rascal" @@ -44,15 +44,6 @@ If you wish to install a specific version of the package, specify the package ve #r "nuget: Rascal, 1.0.1-pre" ``` -For the special flavor of C# script mode used by the [MODiX REPL](https://github.com/discord-csharp/CSharpRepl): - -```cs -#nuget Rascal -``` - -> [!NOTE] -> The MODiX REPL currently doesn't support referencing specific package versions and therefore does not support pre-release versions of the package. - # [`PackageReference`](#tab/csproj) Add under an `ItemGroup` node in your project file: @@ -147,6 +138,12 @@ public sealed class UsersController(IUserService userService) : ControllerBase
+### More samples + +A plethora of additional code samples are available in the [samples](/samples/index.html) section of the documentation. + +
+ ### Explore the API Once you're ready to dive into the library, feel free to refer to the [API documentation](/api/index.html) for an in-depth look into each of the methods provided by the library. You can of course also explore the API through intellisense in your IDE of choice. diff --git a/docs/samples/index.md b/docs/samples/index.md new file mode 100644 index 0000000..9c30202 --- /dev/null +++ b/docs/samples/index.md @@ -0,0 +1,64 @@ +This article contains a variety of code samples demonstrating common usages for various parts of the Rascal library. All the samples source code can be found [here](https://github.com/thinker227/Rascal/tree/main/samples). + +
+ +## Creating results + +Results in Rascal can be created in a variety of ways, the two most common of which are through the [`Ok`](/api/Rascal.Prelude.html#Rascal_Prelude_Ok__1___0_) and [`Err`](/api/Rascal.Prelude.html#Rascal_Prelude_Err__1_Rascal_Error_) methods defined in the prelude, or through implicitly converting ok values or errors into results. + +[!code-csharp[](../../samples/Construction.csx#L7-L13)] + +
+ +## 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`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__). + +[!code-csharp[](../../samples/Map.csx#L6-L14)] + +
+ +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`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___), which can be read as "a, then b, then c". + +[!code-csharp[](../../samples/Then.csx#L6-L19)] + +
+ +[`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) and [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) together make up the core of the [`Result`](/api/Rascal.Result-1.html) type, allowing for chaining multiple operations on a single result. In functional terms, these are what makes [`Result`](/api/Rascal.Result-1.html) a functor and monad respectively (although not an applicative). + +
+ +> [!TIP] +> The aliases [`Select`](/api/Rascal.Result-1.html#Rascal_Result_1_Select__1_System_Func__0___0__) and [`SelectMany`](/api/Rascal.Result-1.html#Rascal_Result_1_SelectMany__1_System_Func__0_Rascal_Result___0___) are available for [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) and [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) respectively. These exist to supply support for *query expressions* as an alternative to method chaining. Query syntax can in specific situations be more readable than the method chaining alternative, although in *most* scenarios, method chaning is better. [`Select`](/api/Rascal.Result-1.html#Rascal_Result_1_Select__1_System_Func__0___0__) and [`SelectMany`](/api/Rascal.Result-1.html#Rascal_Result_1_SelectMany__1_System_Func__0_Rascal_Result___0___) should ***not*** be used outside query syntax. + +
+ +### Combine + +[`Combine`](/api/Rascal.Result-1.html#Rascal_Result_1_Combine__1_Rascal_Result___0__) is an addition to [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) and [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___) 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. + +[!code-csharp[](../../samples/Combine.csx#L6-L15)] + +
+ +## Validation + +Rascal supports a simple way of validating the value of a result, returning an error in case the validation fails. + +[!code-csharp[](../../samples/Validation.csx#L8-L24)] + +
+ +## 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`](/api/Rascal.Prelude.html#Rascal_Prelude_Try__1_System_Func___0__) method in the prelude is the premiere exception-handling method, which runs another function inside a `try`-`catch` block, and returns an [`ExceptionError`](/api/Rascal.Errors.ExceptionError.html) in case an exception is thrown. + +[!code-csharp[](../../samples/Try.csx#L6-L11)] + +
+ +`Try` variants also exist for [`Map`](/api/Rascal.Result-1.html#Rascal_Result_1_Map__1_System_Func__0___0__) and [`Then`](/api/Rascal.Result-1.html#Rascal_Result_1_Then__1_System_Func__0_Rascal_Result___0___), namely [`TryMap`](/api/Rascal.Result-1.html#Rascal_Result_1_TryMap__1_System_Func__0___0__) and [`ThenTry`](/api/Rascal.Result-1.html#Rascal_Result_1_ThenTry__1_System_Func__0_Rascal_Result___0___). + +[!code-csharp[](../../samples/TryMap.csx#L6-L11)] diff --git a/docs/samples/toc.yml b/docs/samples/toc.yml new file mode 100644 index 0000000..fe51488 --- /dev/null +++ b/docs/samples/toc.yml @@ -0,0 +1 @@ +[] diff --git a/docs/toc.yml b/docs/toc.yml index 7385472..c378383 100644 --- a/docs/toc.yml +++ b/docs/toc.yml @@ -1,3 +1,6 @@ +- name: Samples + href: samples/ + homepage: samples/index.md - name: API href: api/ homepage: api/index.md diff --git a/samples/.vscode/launch.json b/samples/.vscode/launch.json new file mode 100644 index 0000000..305245b --- /dev/null +++ b/samples/.vscode/launch.json @@ -0,0 +1,17 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": ".NET Script Debug", + "type": "coreclr", + "request": "launch", + "program": "${env:HOME}/.dotnet/tools/dotnet-script", + "args": ["${file}"], + "windows": { + "program": "${env:USERPROFILE}/.dotnet/tools/dotnet-script.exe", + }, + "cwd": "${workspaceFolder}", + "stopAtEntry": false, + } + ] +} \ No newline at end of file diff --git a/samples/Combine.csx b/samples/Combine.csx new file mode 100644 index 0000000..3dbd10e --- /dev/null +++ b/samples/Combine.csx @@ -0,0 +1,19 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +// 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(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)); + +Console.WriteLine(person); + +record Person(string Name, int Age); diff --git a/samples/Construction.csx b/samples/Construction.csx new file mode 100644 index 0000000..bbad9fa --- /dev/null +++ b/samples/Construction.csx @@ -0,0 +1,15 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using Rascal.Errors; +using static Rascal.Prelude; + +// You can create a result either through explicit Ok/Error functions... +var explicitOk = Ok(new Person("Melody", 27)); +var explicitError = Err("Could not find person"); + +// ... or through implicit conversions... +Result implicitOk = new Person("Edwin", 32); +Result implicitError = new StringError("Failed to find person"); + +record Person(string Name, int Age); diff --git a/samples/Map.csx b/samples/Map.csx new file mode 100644 index 0000000..eaa15e7 --- /dev/null +++ b/samples/Map.csx @@ -0,0 +1,18 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +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(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)); + +Console.WriteLine(person); + +record Person(string Name, int Age); diff --git a/samples/Then.csx b/samples/Then.csx new file mode 100644 index 0000000..e801721 --- /dev/null +++ b/samples/Then.csx @@ -0,0 +1,23 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +// 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(str)); + + // Map the age into a new person. + return age.Map(a => new Person(n, a)); +}); + +Console.WriteLine(person); + +record Person(string Name, int Age); diff --git a/samples/Try.csx b/samples/Try.csx new file mode 100644 index 0000000..a67db88 --- /dev/null +++ b/samples/Try.csx @@ -0,0 +1,14 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +// 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); +}); + +Console.WriteLine(text); diff --git a/samples/TryMap.csx b/samples/TryMap.csx new file mode 100644 index 0000000..bcbb803 --- /dev/null +++ b/samples/TryMap.csx @@ -0,0 +1,13 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using Rascal; +using static Rascal.Prelude; + +// Read console input and assert that it isn't null. +var path = Console.ReadLine().NotNull(); + +// 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)); + +Console.WriteLine(text); diff --git a/samples/Validation.csx b/samples/Validation.csx new file mode 100644 index 0000000..a79d587 --- /dev/null +++ b/samples/Validation.csx @@ -0,0 +1,28 @@ +#r "../src/Rascal/bin/Release/net8.0/Rascal.dll" + +using System.Text.RegularExpressions; +using Rascal; +using Rascal.Errors; +using static Rascal.Prelude; + +// Read console input, assert that it isn't null, and validate that it matches the regex. +var name = Console.ReadLine().NotNull() + .Validate( + 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(str)) + .Validate( + x => x > 0, + _ => "Age has to be greater than 0."); + + return age.Map(a => new Person(n, a)); +}); + +Console.WriteLine(person); + +record Person(string Name, int Age); diff --git a/samples/omnisharp.json b/samples/omnisharp.json new file mode 100644 index 0000000..3789428 --- /dev/null +++ b/samples/omnisharp.json @@ -0,0 +1,6 @@ +{ + "script": { + "enableScriptNuGetReferences": true, + "defaultTargetFramework": "net8.0" + } +} From 99893d3838e697026a0d450827ee9e52195233b4 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 12 Jan 2024 18:20:10 +0100 Subject: [PATCH 18/21] Update readme --- README.md | 179 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 178 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index de7bd5c..155d1ca 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,184 @@ Rascal is first and foremost an aggregate of the result types I personally find
# Installation + +
+.NET CLI + +Run in a terminal in the root of your project: + ```ps1 -# .NET CLI dotnet add package Rascal --prerelease ``` + +
+ +
+Package manager console + +Run from the Visual Studio Package Manager console: + +```ps1 +NuGet\Install-Package Rascal -IncludePrerelease +``` + +
+ +
+Script environment + +In environments such as [C# REPL](https://github.com/waf/CSharpRepl) or [RoslynPad](https://roslynpad.net), enter: + +```cs +#r "nuget: Rascal" +``` + +If you wish to install a specific version of the package, specify the package version: + +```cs +#r "nuget: Rascal, 1.0.1-pre" +``` + +
+ +
+PackageReference + +Add under an `ItemGroup` node in your project file: + +```xml + +``` + +Obviously, replace `1.0.1-pre` with the actual package version you want. + +
+ +
+ +# Samples + +## Creating results + + + +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 +// You can create a result either through explicit Ok/Error functions... +var explicitOk = Ok(new Person("Melody", 27)); +var explicitError = Err("Could not find person"); + +// ... or through implicit conversions... +Result implicitOk = new Person("Edwin", 32); +Result implicitError = new StringError("Failed to find person"); +``` + +## 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`. + +```cs +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(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)); +``` + +
+ +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(str)); + + // Map the age into a new person. + return age.Map(a => new Person(n, a)); +}); +``` + +
+ +`Map` and `Then` together make up the core of the `Result` type, allowing for chaining multiple operations on a single result. In functional terms, these are what makes `Result` 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 +// 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(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( + 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(str)) + .Validate( + x => x > 0, + _ => "Age has to be greater than 0."); + + return age.Map(a => new Person(n, a)); +}); +``` + +## 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. + +```cs +// 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); +}); +``` + +`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(); + +// 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)); +``` From 0c4464955a79bd0535ca4f3840789f77160260ae Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 12 Jan 2024 19:27:38 +0100 Subject: [PATCH 19/21] Update diagnostic categories --- src/Rascal.Analysis/Diagnostics.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Rascal.Analysis/Diagnostics.cs b/src/Rascal.Analysis/Diagnostics.cs index d7415ac..474b739 100644 --- a/src/Rascal.Analysis/Diagnostics.cs +++ b/src/Rascal.Analysis/Diagnostics.cs @@ -2,8 +2,8 @@ namespace Rascal.Analysis; public static class Diagnostics { - private const string CorrectnessCategory = "Correctness"; - private const string AnalysisCategory = "Analysis"; + private const string CorrectnessCategory = "RascalCorrectness"; + private const string AnalysisCategory = "RascalAnalysis"; public static DiagnosticDescriptor UseMap { get; } = new( "RASCAL001", From 08f2bcbeabc4cf43a3d45b5c02c0060a289e2c7b Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 12 Jan 2024 22:11:38 +0100 Subject: [PATCH 20/21] Use better styling for diagnostic info --- docs/diagnostics/rascal001.md | 14 +++++++++----- docs/diagnostics/rascal002.md | 14 +++++++++----- docs/diagnostics/rascal003.md | 14 +++++++++----- docs/diagnostics/rascal004.md | 11 +++++++++-- docs/diagnostics/rascal005.md | 11 +++++++++-- docs/diagnostics/rascal006.md | 14 +++++++++----- docs/diagnostics/rascal007.md | 11 +++++++++-- 7 files changed, 63 insertions(+), 26 deletions(-) diff --git a/docs/diagnostics/rascal001.md b/docs/diagnostics/rascal001.md index e2a4598..2f82235 100644 --- a/docs/diagnostics/rascal001.md +++ b/docs/diagnostics/rascal001.md @@ -1,11 +1,15 @@ # Use 'Map' instead of 'Then(x => Ok(...))' -Id: *RASCAL001* - -Severity: *warning* +
-> [!TIP] -> This diagnostic has an associated automatic code fix. +
+Id: RASCAL001 +
+Severity: warning +
+Has code fix: yes +
+

diff --git a/docs/diagnostics/rascal002.md b/docs/diagnostics/rascal002.md index 13fe139..f17366a 100644 --- a/docs/diagnostics/rascal002.md +++ b/docs/diagnostics/rascal002.md @@ -1,11 +1,15 @@ # Use Then instead of 'Map(...).Unnest()' -Id: *RASCAL002* - -Severity: *warning* +
-> [!TIP] -> This diagnostic has an associated automatic code fix. +
+Id: RASCAL002 +
+Severity: warning +
+Has code fix: yes +
+

diff --git a/docs/diagnostics/rascal003.md b/docs/diagnostics/rascal003.md index 0e21d89..9f6a9a9 100644 --- a/docs/diagnostics/rascal003.md +++ b/docs/diagnostics/rascal003.md @@ -1,11 +1,15 @@ # Unnecessary 'Map' call with identity function -Id: *RASCAL003* - -Severity: *warning* +
-> [!TIP] -> This diagnostic has an associated automatic code fix. +
+Id: RASCAL003 +
+Severity: warning +
+Has code fix: yes +
+

diff --git a/docs/diagnostics/rascal004.md b/docs/diagnostics/rascal004.md index 3e56412..bda1e15 100644 --- a/docs/diagnostics/rascal004.md +++ b/docs/diagnostics/rascal004.md @@ -1,8 +1,15 @@ # 'To' called with same type as result -Id: *RASCAL004* +
-Severity: *warning* +
+Id: RASCAL004 +
+Severity: warning +
+Has code fix: no +
+

diff --git a/docs/diagnostics/rascal005.md b/docs/diagnostics/rascal005.md index 3e4d72e..0a854df 100644 --- a/docs/diagnostics/rascal005.md +++ b/docs/diagnostics/rascal005.md @@ -1,8 +1,15 @@ # 'To' called with impossible type -Id: *RASCAL005* +
-Severity: *warning* +
+Id: RASCAL005 +
+Severity: warning +
+Has code fix: no +
+

diff --git a/docs/diagnostics/rascal006.md b/docs/diagnostics/rascal006.md index 6f439f5..f74555f 100644 --- a/docs/diagnostics/rascal006.md +++ b/docs/diagnostics/rascal006.md @@ -1,11 +1,15 @@ # Use 'GetValueOr' instead of 'Match(x => x, ...)' -Id: *RASCAL006* - -Severity: *warning* +
-> [!TIP] -> This diagnostic has an associated automatic code fix. +
+Id: RASCAL006 +
+Severity: warning +
+Has code fix: yes +
+

diff --git a/docs/diagnostics/rascal007.md b/docs/diagnostics/rascal007.md index f020bf3..ea108a9 100644 --- a/docs/diagnostics/rascal007.md +++ b/docs/diagnostics/rascal007.md @@ -1,8 +1,15 @@ # Missing symbol required for analysis -Id: *RASCAL007* +
-Severity: *warning* +
+Id: RASCAL007 +
+Severity: warning +
+Has code fix: no +
+

From 2cdb9b72d2b4bd7d660155abbc6f3edcd8faa349 Mon Sep 17 00:00:00 2001 From: thinker227 Date: Fri, 12 Jan 2024 22:13:48 +0100 Subject: [PATCH 21/21] Add publish-docs workflow --- .github/workflows/publish-docs.yml | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/publish-docs.yml diff --git a/.github/workflows/publish-docs.yml b/.github/workflows/publish-docs.yml new file mode 100644 index 0000000..180ffd2 --- /dev/null +++ b/.github/workflows/publish-docs.yml @@ -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