Module Relude_Validation

type t('a, 'e) =
| VOk('a)
| VError('e)
;

Similar to result, but has an Applicative instance that collects the errors using a semigroup, rather than fail-fast semantics.

let ok: a e. 'a => t('a'e);

ok() wraps a value in VOk

let error: a e. 'e => t('a'e);

error(val) wraps the value in a VError().

error("Not even") == VError("Not even");
let errorNel: a e. 'e => t('a, Relude_NonEmpty.List.t('e));

Puts an error into a NonEmptyList on the error side of the validation

let errorNea: a e. 'e => t('a, Relude_NonEmpty.Array.t('e));

Puts an error into a NonEmptyArray on the error side of the validation

let isOk: a e. t('a'e) => bool;

isOk(v) returns true if v is of the form VOk(val); false otherwise.

let isError: a e. t('a'e) => bool;

isError(x) returns true if x is of the form VError(val); false otherwise.

let map: a b e. ('a => 'b) => t('a'e) => t('b'e);

map(f, x) returns VOk(f(x)) if x is of the form VOk(v). It returns VError(e) if x is of the form VError(e).

map((x) => {sqrt(float_of_int(x))}, VOk(4)) == VOk(2.0);
map((x) => {sqrt(float_of_int(x))}, VError("bad")) == VError("bad");
let tap: a e. ('a => unit) => t('a'e) => t('a'e);

In tap(f, x), function f() returns unit. Thus, f() is used only for its side effects. If x is of the form VOk(v), tap() calls f(v). The tap() function returns the argument x.

tap(x => Js.log(x), VOk(4)) == VOk(4); // prints 4
tap(x => Js.log(x), VError("bad")) == VError("bad"); // prints nothing
let mapError: a e1 e2. ('e1 => 'e2) => t('a'e1) => t('a'e2);

mapError(f, x) returns VOk(v) if x is of the form VOk(v). It returns VError(f(e)) if x is of the form VError(e).

mapError(x => "Error: " ++ x, VOk(4)) == VOk(4);
mapError(x => "Error: " ++ x, VError("bad")) == VError("Error: bad");
let mapErrorsNea: a e1 e2. ('e1 => 'e2) => t('a, Relude_NonEmpty.Array.t('e1)) => t('a, Relude_NonEmpty.Array.t('e2));

mapErrorsAsNea() applies a function to each error in a NonEmpty.Array of errors in the error channel of the Validation.

let mapErrorsNel: a e1 e2. ('e1 => 'e2) => t('a, Relude_NonEmpty.List.t('e1)) => t('a, Relude_NonEmpty.List.t('e2));

mapErrorsAsNel() applies a function to each error in a NonEmpty.List of errors in the error channel of the Validation.

let tapError: a e. ('e => unit) => t('a'e) => t('a'e);

In tapError(f, x), function f() returns unit. Thus, f() is used only for its side effects. If x is of the form VError(v), tap() calls f(v). The tap() function returns the argument x.

tapError(x => Js.log(x), VOk(4)) == VOk(4); // prints nothing
tapError(x => Js.log(x), VError("bad")) == VError("bad"); // prints "bad"
let bimap: a b e1 e2. ('a => 'b) => ('e1 => 'e2) => t('a'e1) => t('b'e2);

bimap(f, g, x) returns VOk(f(v)) if x is of the form VOk(v); it returns VError(g(e)) if x is of the form VError(e).

let cube = x => x * x * x;
let label = x => "Error: " ++ x;
bimap(cube, label, VOk(12)) == VOk(1728);
bimap(cube, label, VError("bad")) == VError("Error: bad");
let bitap: a e. ('a => unit) => ('e => unit) => t('a'e) => t('a'e);

bitap(f, g, x) the functions f() and g() both return unit, so they are used only for their side effects. If x is of the form VOk(v), bitap() calls f(v); if x is of the form VError(e), bitap() calls g(e). In either case, bitap() returns x.

let printCube = x => Js.log(x * x * x);
let printLabel = x => Js.log("Error: " ++ x);
bitap(printCube, printLabel, VOk(12)) == VOk(12); // prints 1728
bitap(printCube, printLabel, VError("bad")) == VError("bad"); // prints "Error: bad"
let applyWithAppendErrors: a b e. ('e => 'e => 'e) => t('a => 'b'e) => t('a'e) => t('b'e);

apply(valFcn, x, appErrFcn) provides a way of creating a chain of validation functions, accumulating errors along the way.

If valFcn is of the form VOk(f), function f is applied to x. If x is VOk(v), the result is VOk(f(v)). If x is VError(err), the error is passed onwards.

If valFcn is itself of the form VError(err) and x is VOk(v), VError(err) is passed on.

Finally, if both valFcn and x are VError(e1) and VError(e2), the result is VError(appendErrFcn(e1, e2)).

Using apply() properly is somewhat complex. See the example in the __tests__/Relude_Validation_test.re file for more details.

let alignWithAppendErrors: a b e. ('e => 'e => 'e) => t('a'e) => t('b'e) => t(Relude_Ior_Type.t('a'b)'e);
let alignWithWithAppendErrors: a b c e. ('e => 'e => 'e) => (Relude_Ior_Type.t('a'b) => 'c) => t('a'e) => t('b'e) => t('c'e);
let pure: a e. 'a => t('a'e);

pure(val) wraps its argument in a VOk().

pure(3) == VOk(3);
let bind: a b e. t('a'e) => ('a => t('b'e)) => t('b'e);

flatMapV(x, f) returns f(v) when x is of the form VOk(v), and returns x unchanged when it is of the form VError(v).

Note: Validation is not a traditional monad in that it's purpose is to collect errors during applicative validation. Using flatMap will cause all previous errors to be discarded.

let mustBeEven = x =>
  (x mod 2 == 0) ? VOk(x) : VError("not even");

flatMapV(VOk(12), mustBeEven) == VOk(12);
flatMapV(VOk(3), mustBeEven) == VError("not even");
flatMapV(VError("not an int"), mustBeEven) == VError("not an int");
let flatMap: a b e. ('a => t('b'e)) => t('a'e) => t('b'e);

bind is the same as flatMap with the arguments flipped.

let fromResult: Pervasives.result('a'b) => t('a'b);

fromResult converts a value of type result to a Validation.t, returning VOk(x) if the result was Ok(x) and VError(x) if the result was Error(x).

let toResult: t('a'b) => Pervasives.result('a'b);

toResult converts a value of type Validation.t to result.

let fromOption: a e. 'e => option('a) => t('a'e);

fromOption converts an option into a Validation.t, returning VOk(x) if the option was Some(x) and VError (constructed with the provided error) if the option was None.

let fromOptionLazy: a e. (unit => 'e) => option('a) => t('a'e);

fromOptionLazy converts an option into a Validation.t, similar to fromOption, except that provided error is constructed lazily by calling a function. This is useful if the error is expensive to construct, especially since it may not be needed.

let fold: a e c. ('e => 'c) => ('a => 'c) => t('a'e) => 'c;

fold(errFn, okFn, x) returns okFn(v) when the provided Validation (x) is VOk(v); it returns errFn(e) when the Validation is VError(e). This is effectively a function that allows you to "handle" both possible states of the Validation.

let positiveInt = str =>
  Validate.(
    Int.fromtString(str)
    |> fromOption(`InvalidInput)
    |> flatMapV(x => x < 0 ? error(`NotPositive) : pure(x))
  );

let showError =
  fun
  | `InvalidInput => "The provided string was not an int"
  | `NotPositive => "The provided int was negative";

let errMsg = x => "Something went wrong: " ++ showError(x);
let succMsg = x => "Found valid positive int: " ++ Int.toString(x);

// "Something went wrong: The provided string was not an int"
fold(errMsg, succMsg, positiveInt("a"));

// "Something went wrong: The provided int was negative"
fold(errMsg, succMsg, positiveInt("-3"));

// "Found valid positive int: 123"
fold(errMsg, succMsg, positiveInt("123"));
let flip: a e. t('a'e) => t('e'a);

flip Flips the values between the success and error channels.

flip(VOk(12)) == VError(12);
flip(VError(-1)) == VOk(-1);
let map2: ('x => 'x => 'x) => ('a => 'b => 'c) => t('a'x) => t('b'x) => t('c'x);
let map3: ('x => 'x => 'x) => ('a => 'b => 'c => 'd) => t('a'x) => t('b'x) => t('c'x) => t('d'x);
let map4: ('x => 'x => 'x) => ('a => 'b => 'c => 'd => 'e) => t('a'x) => t('b'x) => t('c'x) => t('d'x) => t('e'x);
let map5: ('x => 'x => 'x) => ('a => 'b => 'c => 'd => 'e => 'f) => t('a'x) => t('b'x) => t('c'x) => t('d'x) => t('e'x) => t('f'x);
module WithErrors: (Errors: BsBastet.Interface.SEMIGROUP_ANY) => (Error: BsBastet.Interface.TYPE) => { ... };