trial
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 library for Command Line Interface. |
trial-tomland |
Trial structure integration with the 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:
-
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:
build-depends: base ^>= 4.14
, trial ^>= 0.0
-
In the module where you plan to use Trial
, you should add the import:
import Trial (Trial (..), fiasco, prettyPrintTrial)
-
Now you can use the types and functions from the library:
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:
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:
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 |
The basic example of config problem with the usage of TaggedTrial |
trial-example-advanced |
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:
$ 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:
$ cabal run trial-example -- --host="abc"
$ cabal run trial-example-advanced -- --host="abc"