Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/kowainik/trial

⚖️ Trial Data Type
https://github.com/kowainik/trial

data-structures hacktoberfest haskell haskell-library

Last synced: about 1 month ago
JSON representation

⚖️ Trial Data Type

Awesome Lists containing this project

README

        

# trial

[![GitHub CI](https://github.com/kowainik/trial/workflows/CI/badge.svg)](https://github.com/kowainik/trial/actions)
[![Hackage](https://img.shields.io/hackage/v/trial.svg?logo=haskell)](https://hackage.haskell.org/package/trial)
[![MPL-2.0 license](https://img.shields.io/badge/license-MPL--2.0-blue.svg)](LICENSE)

The `Trial` Data Structure is a `Either`-like structure that keeps events history
inside. The data type allows to keep track of the `Fatality` level of each such
event entry (`Warning` or `Error`).

## Project Structure

This is a multi-package project that has the following packages inside:

| Package | Description |
|------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `trial` | The main package that contains the `Trial` data structure, instances and useful functions to work with the structure. |
| `trial-optparse-applicative` | `Trial` structure integration with the [`optparse-applicative`](https://hackage.haskell.org/package/optparse-applicative) library for Command Line Interface. |
| `trial-tomland` | `Trial` structure integration with the [`tomland`](https://hackage.haskell.org/package/tomland) library for TOML configurations. |
| `trial-example` | Example project with the usage example of the `Trial` data structure. |

## How to use `trial`

`trial` is compatible with the latest GHC versions starting from `8.6.5`.

In order to start using `trial` in your project, you will need to set it up with
the three easy steps:

1. Add the dependency on `trial` in your project's `.cabal` file. For this, you
should modify the `build-depends` section by adding the name of this library.
After the adjustment, this section could look like this:

```haskell
build-depends: base ^>= 4.14
, trial ^>= 0.0
```

2. In the module where you plan to use `Trial`, you should add the import:

```haskell
import Trial (Trial (..), fiasco, prettyPrintTrial)
```

3. Now you can use the types and functions from the library:

```haskell
main :: IO ()
main = putStrLn $ prettyPrintTrial $ fiasco "This is fiasco, bro!"
```

## Trial Data Structure

Let's have a closer look at the `Trial` data structure.
`Trial` is a sum type that has two constructors:

- `Fiasco` — represents the unsuccessful state similar to the `Left`
constructor of `Either`. However, unlike `Left`, `Fiasco` holds a list of all
`error`-like items that happened along the way. Each such item has a notion of
`Fatality` (the severity of the error). The following cases cover
`Fatality`:
+ `Error` — fatal error that led to the final fatal `Fiasco`.
+ `Warning` — non-essential error, which didn't affect the result.
- `Result` — represents the successful state similar to the `Right`
constructor of `Either`. However, unlike `Right`, `Result` keeps the list of
all `error`-like items that happened along the way. All error items are
warnings as the final result was found anyway.

Schematically, `Trial` has the following internal representation:

```haskell
data Trial e a
│ │
│ ╰╴Resulting type

╰╴An error item type

-- | Unsuccessful case
= Fiasco (DList (Fatality, e))
│ │ │
│ │ ╰╴One error item
│ │
│ ╰╴Level of damage

╰╴Efficient list-container for error type items

-- | Successful case
| Result (DList e) a
│ │ │
│ │ ╰╴Result
│ │
│ ╰╴One warning item

╰╴Efficient list-container for warning type items
```

### `Trial` instances

In order to follow the basis idea of the data type, `Trial` uses smart
constructors and different instances to make the structure work the way it
works.

Here are the main points:

* All `Fiasco`s can be created only with the `Error` `Fatality` level.
* The `Fatality` level can be eased only through the `Semigroup` appends of
different `Trial`s.
* All error items in `Result` should have only `Warning` `Fatality` level. This
is guaranteed by the `Trial` `Semigroup` and `Applicative` instances.
* `Semigroup` is responsible for the correct collection of history events, their
`Fatality` level and the final result decision.
* `Semigroup` chooses the latest 'Result' and combines all events.
* Think of `Semigroup` instance as of high-level combinator of your result.
* `Applicative` is responsible for the correct combination of `Trial`s.
* `Applicative` returns `Fiasco`, if at least one value if `Fiasco`, combine all
events.
* Think of `Applicative` instance as of low-level combinator of your result on the
record fields level.
* `Alternative` instance could help when you want to stop on the first
`Result` and get the history of all failures before it.
* `Alternative`: return first `Result`, also combine all events for
all `Trial`s before this `Result`.

## Tagged `Trial`

Additionally, there is a `Trial`-like data type that has a notion of the `tag`
inside.

The main difference from `Trial` is that the resulting type contains additional
information of the tag (or source it came from). The type looks like this:

```haskell
type TaggedTrial tag a = Trial tag (tag, a)
```

Due to the described instances implementation, the tag will always be aligned
with the final source it came from.

The library provides different ways to add the tag:
* Manual with the `withTag` function
* Using `OverloadedLabels`and the provided `IsLabel` instance for
`TaggedTrial`.

You can choose the one that is more suitable for your use-case.

## Usage Examples

One of the use cases when one could consider using `Trial` is the configurations
in the application.

If you need to collect configurations from different places, combine the results
into a single configuration, you can find the `Trial` data structure quite
handy. With `trial` you can get the event history for free and also you can keep
track of where the final result for each component of your configurations type
comes from (by using `tag` functionality).

The complete example in the `trial-example` package. It combines CLI, TOML
configuration and the default options provided in the source code.

| Executable | Description |
|------------|-------------|
| [`trial-example`](https://github.com/kowainik/trial/blob/main/trial-example/app/Main.hs) | The basic example of config problem with the usage of `TaggedTrial` |
| [`trial-example-advanced`](https://github.com/kowainik/trial/blob/main/trial-example/app-advanced/Main.hs) | The basic example of config problem with the usage of `TaggedTrial` with the `Phase` based approach. |

To run it you can use the following command:

```shell
$ cabal run trial-example
$ cabal run trial-example-advanced
```

For the successful result you can use the CLI and provide necessary information
in order to have the complete configurations:

```shell
$ cabal run trial-example -- --host="abc"
$ cabal run trial-example-advanced -- --host="abc"
```