Last active
January 14, 2025 22:35
-
-
Save pblasucci/7dee5f662956eaaa2ece07dcd9d6488c to your computer and use it in GitHub Desktop.
F# FaultReport
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace pblasucci.FaultReport | |
/// Minimal contract provided by any failure. | |
[<Interface>] | |
type IFault = | |
/// An unstructured human-readable summary of the current failure. | |
abstract Message : string | |
/// An optional reference to the failure which triggered the current failure | |
/// (n.b. most failure do NOT have a cause). | |
abstract Cause : IFault option | |
/// <summary> | |
/// Represents the outcome of an operation which maybe have either: passed, or failed. | |
/// An instance of this type is guaranteed to only ever be in one state or the other. | |
/// Additionally, either state may carry additional data (n.b. for failed outcomes, the additional | |
/// data must implement the <see cref="T:pblasucci.FaultReport.IFault"/> contract). | |
/// </summary> | |
type Report<'Pass, 'Fail when 'Fail :> IFault> = | |
/// Represents the successful outcome of an operation (i.e. it passed). | |
| Pass of value : 'Pass | |
/// Represents the unsuccessful outcome of an operation (i.e. it failed). | |
| Fail of fault : 'Fail | |
/// <summary> | |
/// Contains active patterns for working with <see cref="T:pblasucci.FaultReport.IReport`1"/> instances. | |
/// </summary> | |
/// <remarks>This module is automatically opened.</remarks> | |
[<AutoOpen>] | |
module Patterns = | |
/// <summary> | |
/// Matches an <see cref="T:pblasucci.FaultReport.Report`2"/> instance, where the | |
/// failing case has been generalized to <see cref="T:pblasucci.FaultReport.IFault"/>, | |
/// only succeeding when said report has failed AND the extra failure data | |
/// expressly matches the given output. | |
/// </summary> | |
/// <param name="report">The report against which to match.</param> | |
/// <remarks> | |
/// The target failure must implement the <see cref="T:pblasucci.FaultReport.IFault"/> contract. | |
/// Using this active pattern often requires an explicit type annotation. | |
/// </remarks> | |
/// <example> | |
/// <c>(|FailAs|_|)</c> can be used to extract a specific failure from an <c>IReport</c>, | |
/// which typically expresses failure as the base contract, <c>IFault</c>. | |
/// <code lang="fsharp"> | |
/// match report with | |
/// | Pass value -> ... | |
/// | FailAs(fault : SpecificFailure) -> ... | |
/// | Fail fault -> ... | |
/// </code> | |
/// </example> | |
let inline (|FailAs|_|) | |
(report : Report<'Pass, IFault>) | |
: 'Fail option when 'Fail :> IFault | |
= | |
match report with | |
| Fail(:? 'Fail as fault) -> Some fault | |
| _ -> None | |
/// <summary> | |
/// Contains utilities for working with <see cref="T:pblasucci.FaultReport.Report`2"/> instances. | |
/// </summary> | |
[<RequireQualifiedAccess>] | |
module Report = | |
/// Executes the given function against the value contained in a passing Report; | |
/// otherwise, return the original (failing Report). | |
let inline bind | |
(pass : 'Pass -> Report<'T, 'Fail>) | |
(report : Report<'Pass, 'Fail>) | |
: Report<'T, 'Fail> | |
= | |
match report with | |
| Pass value -> pass value | |
| Fail error -> Fail error | |
/// Executes the given function against the value contained in a failing Report; | |
/// otherwise, return the original (passing Report). | |
let inline bindFail | |
(fail : 'Fail -> Report<'Pass, 'T>) | |
(report : Report<'Pass, 'Fail>) | |
: Report<'Pass, 'T> | |
= | |
match report with | |
| Pass value -> Pass value | |
| Fail error -> fail error | |
/// <summary> | |
/// Corrects the fault-type of a Report to be <see cref="T:pblasucci.FaultReport.IFault"/>. | |
/// </summary> | |
let inline generalize | |
(report : Report<'Pass, 'Fail>) | |
: Report<'Pass, IFault> | |
= | |
report |> bindFail (fun fault -> Fail(upcast fault)) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace pblasucci.FaultReport | |
open System.Collections | |
open System.Collections.Generic | |
[<AutoOpen>] | |
module Library = | |
open System | |
let inline ``panic!`` (message : string) : 'T = | |
try | |
raise (InvalidProgramException message) | |
with x -> | |
Environment.FailFast("Fatal error; program must exit!", x) | |
Unchecked.defaultof<'T> | |
[<Interface>] | |
type IFault = | |
abstract Message : string | |
abstract Cause : IFault option | |
type Demotion<'X when 'X :> exn>(source : 'X, ?message : string) = | |
member _.Source : 'X = source | |
member val Message : string = defaultArg message source.Message | |
interface IFault with | |
member me.Message = me.Message | |
member _.Cause = None | |
type Faulty<'T when 'T : (member Message : string)> = 'T | |
[<RequireQualifiedAccess>] | |
module Fault = | |
let demote (source : 'X :> exn) = Demotion<'X>(source) | |
let inline derive<'T when Faulty<'T>> (faulty : 'T) : IFault = | |
match box faulty with | |
| :? IFault as fault -> fault | |
| :? exn as source -> Demotion source | |
| _ -> | |
{ new IFault with | |
member _.Message = faulty.Message | |
member _.Cause = None | |
} | |
let promote (toExn : IFault -> 'X) (fault : IFault) : 'X :> exn = toExn fault | |
let escalate (toExn : IFault -> 'X) (fault : IFault) : 'X :> exn = | |
fault |> promote toExn |> raise | |
type Report<'Pass, 'Fail when 'Fail :> IFault> = | |
| Pass of value : 'Pass | |
| Fail of fault : 'Fail | |
static member op_Implicit | |
(report : Report<'Pass, 'Fail>) | |
: Result<'Pass, 'Fail> | |
= | |
match report with | |
| Pass(value : 'Pass) -> Ok value | |
| Fail(error : 'Fail) -> Error error | |
static member op_Implicit | |
(result : Result<'Pass, 'Fail>) | |
: Report<'Pass, 'Fail> | |
= | |
match result with | |
| Ok(value : 'Pass) -> Pass value | |
| Error(error : 'Fail) -> Fail error | |
[<AutoOpen>] | |
module Patterns = | |
let inline (|FailAs|_|) | |
(report : Report<'Pass, IFault>) | |
: 'Fail option when 'Fail :> IFault | |
= | |
match report with | |
| Fail(:? 'Fail as fault) -> Some fault | |
| Fail _ | |
| Pass _ -> None | |
let inline (|DemotedAs|_|) (fault : IFault) : 'X option when 'X :> exn = | |
match fault with | |
| :? Demotion<'X> as x -> Some x.Source | |
| :? Demotion<exn> as x when (x.Source :? 'X) -> Some(downcast x.Source) | |
| _ -> None | |
let inline (|Demoted|_|) (report : Report<'Pass, IFault>) = | |
match report with | |
| Fail(DemotedAs(demoted : 'X)) -> Some demoted | |
| _ -> None | |
[<RequireQualifiedAccess>] | |
module Report = | |
let ofFault (fault : IFault) : Report<'Pass, IFault> = Fail fault | |
let ofExn (fault : 'X) : Report<'Pass, Demotion<'X>> = | |
fault |> Demotion<_> |> Fail | |
let inline bind | |
(pass : 'Pass -> Report<'T, 'Fail>) | |
(report : Report<'Pass, 'Fail>) | |
: Report<'T, 'Fail> | |
= | |
match report with | |
| Pass value -> pass value | |
| Fail error -> Fail error | |
let inline bindFail | |
(fail : 'Fail -> Report<'Pass, 'T>) | |
(report : Report<'Pass, 'Fail>) | |
: Report<'Pass, 'T> | |
= | |
match report with | |
| Pass value -> Pass value | |
| Fail error -> fail error | |
let map | |
(pass : 'Pass -> 'T) | |
(report : Report<'Pass, 'Fail>) | |
: Report<'T, 'Fail> | |
= | |
report |> bind (pass >> Pass) | |
let mapFail | |
(fail : 'Fail -> 'T) | |
(report : Report<'Pass, 'Fail>) | |
: Report<'Pass, 'T> | |
= | |
report |> bindFail (fail >> Fail) | |
let inline generalize | |
(report : Report<'Pass, 'Fail>) | |
: Report<'Pass, IFault> | |
= | |
report |> mapFail (fun fault -> upcast fault) | |
let iter (pass : 'Pass -> unit) (report : Report<'Pass, 'Fail>) : unit = | |
match report with | |
| Pass value -> pass value | |
| Fail _ -> ( (* noop *) ) | |
let iterFail (fail : 'Fail -> unit) (report : Report<'Pass, 'Fail>) : unit = | |
match report with | |
| Pass _ -> ( (* noop *) ) | |
| Fail fault -> fail fault | |
let isPass (report : Report<'Pass, 'Fail>) : bool = | |
match report with | |
| Pass _ -> true | |
| Fail _ -> false | |
let isFail (report : Report<'Pass, 'Fail>) : bool = | |
match report with | |
| Pass _ -> false | |
| Fail _ -> true | |
let toResult (report : Report<'Pass, 'Fail>) : Result<'Pass, 'Fail> = | |
Report.op_Implicit report | |
let ofResult (result : Result<'Pass, 'Fail>) : Report<'Pass, 'Fail> = | |
Report.op_Implicit result | |
let toOption (report : Report<'Pass, 'Fail>) : 'Pass option = | |
match report with | |
| Pass value -> Some value | |
| Fail _ -> None | |
let ofOption | |
(withFault : unit -> 'Fail) | |
(option : 'Pass option) | |
: Report<'Pass, 'Fail> | |
= | |
match option with | |
| Some value -> Pass value | |
| None -> Fail(withFault ()) | |
let toChoice (report : Report<'Pass, 'Fail>) : Choice<'Pass, 'Fail> = | |
match report with | |
| Pass value -> Choice1Of2 value | |
| Fail fault -> Choice2Of2 fault | |
let ofChoice (choice : Choice<'Pass, 'Fail>) : Report<'Pass, 'Fail> = | |
match choice with | |
| Choice1Of2 value -> Pass value | |
| Choice2Of2 fault -> Fail fault | |
let defaultValue (value : 'Pass) (report : Report<'Pass, 'Fail>) : 'Pass = | |
match report with | |
| Pass value' -> value' | |
| Fail _ -> value | |
let defaultWith | |
(withFault : 'Fail -> 'Pass) | |
(report : Report<'Pass, 'Fail>) | |
: 'Pass | |
= | |
match report with | |
| Pass value -> value | |
| Fail fault -> withFault fault | |
[<Sealed>] | |
type CompoundFault(faults : IFault seq, ?message : string, ?cause : IFault) = | |
do (* .ctor *) | |
if isNull faults then | |
nullArg (nameof faults) | |
elif Seq.length faults < 1 then | |
invalidArg (nameof faults) "Must provide at least one fault." | |
member val Faults : IFault seq = faults |> Seq.toArray |> Seq.readonly | |
member val Message : string = defaultArg message "One or more errors occurred" | |
interface IFault with | |
member me.Message = me.Message | |
member _.Cause = cause | |
interface IEnumerable<IFault> with | |
member me.GetEnumerator() = me.Faults.GetEnumerator() | |
member me.GetEnumerator() = (me.Faults :> IEnumerable).GetEnumerator() | |
[<RequireQualifiedAccess>] | |
module Array = | |
let divide (items : Report<'Pass, 'Fail> array) : 'Pass array * 'Fail array = | |
if isNull items then | |
nullArg (nameof items) | |
elif 0 = Array.length items then | |
(Array.empty, Array.empty) | |
else | |
let passing, failing = ResizeArray<'Pass>(), ResizeArray<_>() | |
for item in items do | |
match item with | |
| Pass value -> passing.Add(value) | |
| Fail fault -> failing.Add(fault) | |
(passing.ToArray(), failing.ToArray()) | |
let accumulate | |
(project : 'T -> Report<'Pass, IFault>) | |
(items : 'T array) | |
: Report<'Pass array, CompoundFault> | |
= | |
if isNull items then | |
nullArg (nameof items) | |
elif 0 = Array.length items then | |
Pass Array.empty | |
else | |
let passing, failing = ResizeArray<'Pass>(), ResizeArray<_>() | |
for item in items do | |
match project item with | |
| Pass value -> passing.Add(value) | |
| Fail error -> failing.Add(error) | |
if 0 < failing.Count then | |
Fail(CompoundFault failing) | |
else | |
Pass(passing.ToArray()) | |
let traverse | |
(project : 'T -> Report<'Pass, IFault>) | |
(items : 'T array) | |
: Report<'Pass array, IFault> | |
= | |
if isNull items then | |
nullArg (nameof items) | |
elif 0 = Array.length items then | |
Pass Array.empty | |
else | |
let buffer = ResizeArray<'Pass>() | |
let mutable halted = ValueOption.None | |
let enum = (items :> 'T seq).GetEnumerator() | |
while ValueOption.isNone halted && enum.MoveNext() do | |
match project enum.Current with | |
| Pass value -> buffer.Add(value) | |
| Fail error -> halted <- ValueSome error | |
match halted with | |
| ValueSome error -> Fail error | |
| ValueNone -> Pass(buffer.ToArray()) | |
let sequence | |
(reports : Report<'Pass, IFault> array) | |
: Report<'Pass array, IFault> | |
= | |
reports |> traverse id | |
[<RequireQualifiedAccess>] | |
module List = | |
let divide (items : Report<'Pass, 'Fail> list) : 'Pass list * 'Fail list = | |
let passing, failing = items |> List.toArray |> Array.divide | |
(List.ofArray passing, List.ofArray failing) | |
let accumulate | |
(project : 'T -> Report<'Pass, IFault>) | |
(items : 'T list) | |
: Report<'Pass list, CompoundFault> | |
= | |
items |> List.toArray |> Array.accumulate project |> Report.map Array.toList | |
let traverse | |
(project : 'T -> Report<'Pass, IFault>) | |
(items : 'T list) | |
: Report<'Pass list, IFault> | |
= | |
items |> List.toArray |> Array.traverse project |> Report.map Array.toList | |
let sequence | |
(reports : Report<'Pass, IFault> list) | |
: Report<'Pass list, IFault> | |
= | |
reports |> traverse id | |
[<RequireQualifiedAccess>] | |
module Seq = | |
let divide (items : Report<'Pass, 'Fail> seq) : 'Pass seq * 'Fail seq = | |
let passing, failing = items |> Seq.toArray |> Array.divide | |
(Seq.ofArray passing, Seq.ofArray failing) | |
let accumulate | |
(project : 'T -> Report<'Pass, IFault>) | |
(items : 'T seq) | |
: Report<'Pass seq, CompoundFault> | |
= | |
items |> Seq.toArray |> Array.accumulate project |> Report.map Array.toSeq | |
let traverse | |
(project : 'T -> Report<'Pass, IFault>) | |
(items : 'T seq) | |
: Report<'Pass seq, IFault> | |
= | |
items |> Seq.toArray |> Array.traverse project |> Report.map Array.toSeq | |
let sequence | |
(reports : Report<'Pass, IFault> seq) | |
: Report<'Pass seq, IFault> | |
= | |
reports |> traverse id |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
namespace pblasucci.FaultReport | |
/// <summary> | |
/// Contains general-purpose failure-related utilities. | |
/// </summary> | |
/// <remarks>This module is automatically opened.</remarks> | |
[<AutoOpen>] | |
module Library = | |
/// Stop the world -- I want to get off! | |
val inline ``panic!`` : message : string -> 'T | |
/// Minimal contract provided by any failure. | |
[<Interface>] | |
type IFault = | |
/// An unstructured human-readable summary of the current failure. | |
abstract Message : string | |
/// An optional reference to the failure which triggered the current failure | |
/// (n.b. most failure do NOT have a cause). | |
abstract Cause : IFault option | |
/// A CLR exception which has been trapped and reduced to a failure. | |
type Demotion<'X when 'X :> exn> = | |
interface IFault | |
/// <summary> | |
/// Creates a new <c>Demotion<'T></c> from the given subtype of <see cref="T:System.Exception"/> | |
/// and, optionally, an unstructured human-readable summary of the current failure. | |
/// </summary> | |
/// <param name="source">The exception being demoted.</param> | |
/// <param name="message">An unstructured human-readable summary of the demotion.</param> | |
new : source : 'X * ?message : string -> Demotion<'X> | |
/// <summary> | |
/// A subtype of <see cref="T:System.Exception"/> which has been trapped and reduced to a failure. | |
/// </summary> | |
member Source : 'X | |
/// An unstructured human-readable summary of the demotion. | |
member Message : string | |
/// <summary> | |
/// A type abbreviation which helps with the automatic derivation of <see cref="T:pblasucci.FaultReport.IFault"/> instances. | |
/// </summary> | |
type Faulty<'T when 'T : (member Message : string)> = 'T | |
/// <summary> | |
/// Contains utilities for working with <see cref="T:pblasucci.FaultReport.IFault"/> instances. | |
/// </summary> | |
[<RequireQualifiedAccess>] | |
module Fault = | |
/// <summary> | |
/// Creates a new <see cref="T:pblasucci.FaultReport.IFault"/> instance from any "fault-like" values, | |
/// where "fault-like" means: "has a public property, named <c>Message</c>, of type <see cref="T:System.String"/>. | |
/// </summary> | |
val inline derive : faulty : ^T -> IFault when ^T : (member Message : string) | |
/// Reduces a captured exception to a failure. | |
val demote : source : 'X -> Demotion<'X> when 'X :> exn | |
/// Elevates a failure to a throwable exception. | |
val promote : toExn : (IFault -> 'X) -> fault : IFault -> 'X when 'X :> exn | |
/// Elevates a failure to a throwable exception, and immediately raises said exception. | |
val escalate : toExn : (IFault -> 'X) -> fault : IFault -> 'X when 'X :> exn | |
/// <summary> | |
/// Represents the outcome of an operation which maybe have either: passed, or failed. | |
/// An instance of this type is guaranteed to only ever be in one state or the other. | |
/// Additionally, either state may carry additional data (n.b. for failed outcomes, the additional | |
/// data must implement the <see cref="T:pblasucci.FaultReport.IFault"/> contract). | |
/// </summary> | |
type Report<'Pass, 'Fail when 'Fail :> IFault> = | |
/// Represents the successful outcome of an operation (i.e. it passed). | |
| Pass of value : 'Pass | |
/// Represents the unsuccessful outcome of an operation (i.e. it failed). | |
| Fail of fault : 'Fail | |
static member op_Implicit : | |
report : Report<'Pass, 'Fail> -> Result<'Pass, 'Fail> | |
static member op_Implicit : | |
result : Result<'Pass, 'Fail> -> Report<'Pass, 'Fail> | |
/// <summary> | |
/// Contains active patterns for working with <see cref="T:pblasucci.FaultReport.IReport`1"/> instances. | |
/// </summary> | |
/// <remarks>This module is automatically opened.</remarks> | |
[<AutoOpen>] | |
module Patterns = | |
/// <summary> | |
/// Matches an <see cref="T:pblasucci.FaultReport.Report`2"/> instance, where the | |
/// failing case has been generalized to <see cref="T:pblasucci.FaultReport.IFault"/>, | |
/// only succeeding when said report has failed AND the extra failure data | |
/// expressly matches the given output. | |
/// </summary> | |
/// <param name="report">The report against which to match.</param> | |
/// <remarks> | |
/// The target failure must implement the <see cref="T:pblasucci.FaultReport.IFault"/> contract. | |
/// Using this active pattern often requires an explicit type annotation. | |
/// </remarks> | |
/// <example> | |
/// <c>(|FailAs|_|)</c> can be used to extract a specific failure from an <c>IReport</c>, | |
/// which typically expresses failure as the base contract, <c>IFault</c>. | |
/// <code lang="fsharp"> | |
/// match report with | |
/// | Pass value -> ... | |
/// | FailAs(fault : SpecificFailure) -> ... | |
/// | Fail fault -> ... | |
/// </code> | |
/// </example> | |
val inline (|FailAs|_|) : | |
report : Report<'Pass, IFault> -> 'Fail option when 'Fail :> IFault | |
/// <summary> | |
/// Matches an <see cref="T:pblasucci.FaultReport.Report`2"/> instance, where the | |
/// failing case has been generalized to <see cref="T:pblasucci.FaultReport.IFault"/>, | |
/// only succeeding when said report has failed AND the extra failure data | |
/// is expressly a demotion of the given exception subtype. | |
/// </summary> | |
/// <param name="report">The report against which to match.</param> | |
/// <remarks> | |
/// The target failure must subclass <see cref="T:System.Exception"/>. | |
/// Using this active pattern typically requires an explicit type annotation. | |
/// </remarks> | |
/// <example> | |
/// <c>(|Demoted|_|)</c> can be used to extract a specific exception from an <c>IReport</c>, | |
/// which typically expresses failure as the base contract, <c>IFault</c>. | |
/// <code lang="fsharp"> | |
/// match report with | |
/// | Pass value -> ... | |
/// | Demoted(fault : TimeoutException) -> ... | |
/// | Fail fault -> ... | |
/// </code> | |
/// </example> | |
val inline (|Demoted|_|) : | |
report : Report<'Pass, IFault> -> 'X option when 'X :> exn | |
/// <summary> | |
/// Matches an <see cref="T:pblasucci.FaultReport.IFault"/> instance, | |
/// only succeeding when said fault is expressly a demotion of the given exception subtype, | |
/// or is a demotion whose source is castable to the given exception subtype. | |
/// </summary> | |
/// <param name="fault">The fault against which to match.</param> | |
/// <remarks> | |
/// The target failure must subclass <see cref="T:System.Exception"/>. | |
/// Using this active pattern typically requires an explicit type annotation. | |
/// </remarks> | |
/// <example> | |
/// <c>(|DemotionOf|_|)</c> can be used to extract a specific exception from an <c>IFault</c>, | |
/// assuming its concretion is assuming its concretion is <see cref="T:pblasucci.FaultReport.Demotion`1"/>. | |
/// <code lang="fsharp"> | |
/// match fault with | |
/// | DemotionOf(fault : TimeoutException) -> ... | |
/// | _ -> ... | |
/// </code> | |
/// </example> | |
val inline (|DemotedAs|_|) : fault : IFault -> 'X option when 'X :> exn | |
/// <summary> | |
/// Contains utilities for working with <see cref="T:pblasucci.FaultReport.Report`2"/> instances. | |
/// </summary> | |
[<RequireQualifiedAccess>] | |
module Report = | |
/// Executes the given function against the value contained in a passing Report; | |
/// otherwise, return the original (failing Report). | |
val inline bind : | |
pass : ('Pass -> Report<'T, 'Fail>) -> | |
report : Report<'Pass, 'Fail> -> | |
Report<'T, 'Fail> | |
when 'Fail :> IFault | |
/// Executes the given function against the value contained in a failing Report; | |
/// otherwise, return the original (passing Report). | |
val inline bindFail : | |
fail : ('Fail -> Report<'Pass, 'T>) -> | |
report : Report<'Pass, 'Fail> -> | |
Report<'Pass, 'T> | |
when 'Fail :> IFault and 'T :> IFault | |
/// Executes the given function against the value contained in a passing Report; | |
/// otherwise, return the original (failing Report). | |
val map : | |
pass : ('Pass -> 'T) -> report : Report<'Pass, 'Fail> -> Report<'T, 'Fail> | |
when 'Fail :> IFault | |
/// Executes the given function against the value contained in a failing Report; | |
/// otherwise, return the original (passing Report). | |
val mapFail : | |
fail : ('Fail -> 'T) -> report : Report<'Pass, 'Fail> -> Report<'Pass, 'T> | |
when 'Fail :> IFault and 'T :> IFault | |
/// <summary> | |
/// Corrects the fault-type of a Report to be <see cref="T:pblasucci.FaultReport.IFault"/>. | |
/// </summary> | |
val inline generalize : | |
report : Report<'Pass, #IFault> -> Report<'Pass, IFault> | |
/// Executes the given action if, and only if, the given Report has passed. | |
val iter : pass : ('Pass -> unit) -> report : Report<'Pass, #IFault> -> unit | |
/// Executes the given action if, and only if, the given Report has failed. | |
val iterFail : | |
fail : ('Fail -> unit) -> report : Report<'Pass, 'Fail> -> unit | |
when 'Fail :> IFault | |
/// <summary> | |
/// Returns <c>true</c> if the given Report has passed; otherwise, returns <c>false</c>. | |
/// </summary> | |
val isPass : report : Report<'Pass, #IFault> -> bool | |
/// <summary> | |
/// Returns <c>true</c> if the given Report has failed; otherwise, returns <c>false</c>. | |
/// </summary> | |
val isFail : report : Report<'Pass, #IFault> -> bool | |
/// <summary> | |
/// Converts the given report to an instance of <see cref="T:Microsoft.FSharp.Core.FSharpResult`2"/>. | |
/// </summary> | |
val toResult : | |
report : Report<'Pass, 'Fail> -> Result<'Pass, 'Fail> when 'Fail :> IFault | |
/// <summary> | |
/// Builds a report instance from the given <see cref="T:Microsoft.FSharp.Core.FSharpResult`2"/>. | |
/// </summary> | |
val ofResult : | |
result : Result<'Pass, 'Fail> -> Report<'Pass, 'Fail> when 'Fail :> IFault | |
/// <summary> | |
/// Converts the given report to an instance of <see cref="T:Microsoft.FSharp.Core.FSharpOption`1"/>. | |
/// </summary> | |
val toOption : report : Report<'Pass, #IFault> -> 'Pass option | |
/// <summary> | |
/// Builds a report instance from the given <see cref="T:Microsoft.FSharp.Core.FSharpOption`1"/>, | |
/// using the given factory function to create fault data if the input option is <c>None</c>. | |
/// </summary> | |
val ofOption : | |
withFault : (unit -> 'Fail) -> option : 'Pass option -> Report<'Pass, 'Fail> | |
when 'Fail :> IFault | |
/// <summary> | |
/// Converts the given report to an instance of <see cref="T:Microsoft.FSharp.Core.FSharpChoice`2"/>. | |
/// </summary> | |
val toChoice : | |
report : Report<'Pass, 'Fail> -> Choice<'Pass, 'Fail> when 'Fail :> IFault | |
/// <summary> | |
/// Builds a report instance from the given <see cref="T:Microsoft.FSharp.Core.FSharpChoice`2"/>. | |
/// </summary> | |
val ofChoice : | |
choice : Choice<'Pass, 'Fail> -> Report<'Pass, 'Fail> when 'Fail :> IFault | |
/// For a passing Report, returns the underlying value, | |
/// but returns the given value for a failing Report. | |
val defaultValue : value : 'Pass -> report : Report<'Pass, #IFault> -> 'Pass | |
/// For a passing Report, returns the underlying value, | |
/// but returns the result of the given function for a failing Report. | |
val defaultWith : | |
withFault : ('Fail -> 'Pass) -> report : Report<'Pass, 'Fail> -> 'Pass | |
when 'Fail :> IFault | |
/// <summary> | |
/// Builds a failing report instance from the given <see cref="T:pblasucci.FaultReport.IFault"/>. | |
/// </summary> | |
val ofFault : fault : IFault -> Report<'Pass, IFault> | |
/// <summary> | |
/// Builds a failing report instance from the given <see cref="T:System.Exception"/> | |
/// (or one of its many subclasses). | |
/// </summary> | |
val ofExn : fault : 'X -> Report<'Pass, Demotion<'X>> when 'X :> exn | |
/// <summary> | |
/// An <see cref="T:pblasucci.FaultReport.IFault"/>, which contains nested <c>IFault</c> instances | |
/// (note: this type is necessary for certain failure-aggregation scenarios). | |
/// </summary> | |
[<Sealed>] | |
type CompoundFault = | |
interface IFault | |
interface System.Collections.Generic.IEnumerable<IFault> | |
new : | |
faults : IFault seq * ?message : string * ?cause : IFault -> CompoundFault | |
member Faults : IFault seq | |
member Message : string | |
/// <summary> | |
/// Tools for working with <see cref="T:pblasucci.FaultReport.Report`2"/> | |
/// in conjunction with <see cref='T:Microsoft.FSharp.Core.array`1'/>. | |
/// </summary> | |
[<RequireQualifiedAccess>] | |
module Array = | |
/// Splits a collection of reports into two collections: | |
/// one containing only the passing value; | |
/// and, one containing only the failing values. | |
val divide : | |
items : Report<'Pass, 'Fail> array -> 'Pass array * 'Fail array | |
when 'Fail :> IFault | |
/// <summary> | |
/// Applies the given function to each of the given items, | |
/// accumulating either the passing data or the faults into a single | |
/// <see cref="T:pblasucci.FaultReport.Report`2"/> instance. | |
/// </summary> | |
/// <remarks> | |
/// The <c>Report</c> instance returned from this function will only be in | |
/// a passing state if all the input produced passing values (when the | |
/// given input function is applied to each item). Further, all items of the | |
/// given input collection will always be iterated (ie: all possible failures | |
/// will be collected). | |
/// </remarks> | |
val accumulate : | |
project : ('T -> Report<'Pass, IFault>) -> | |
items : 'T array -> | |
Report<'Pass array, CompoundFault> | |
/// <summary> | |
/// Applies the given function to each of the given items, | |
/// accumulating either the passing data or the first fault into a single | |
/// <see cref="T:pblasucci.FaultReport.Report`2"/> instance. | |
/// </summary> | |
/// <remarks> | |
/// The <c>Report</c> instance returned from this function will only be in | |
/// a passing state if all the input produced passing values (when the | |
/// given input function is applied to each item). Further, all items of the | |
/// given input collection may not be iterated (ie: processing will stop after | |
/// the first failing value is detected). | |
/// </remarks> | |
val traverse : | |
project : ('T -> Report<'Pass, IFault>) -> | |
items : 'T array -> | |
Report<'Pass array, IFault> | |
/// Turns a collection of report instances into a single report containing either: | |
/// each of the passing values; or, the first fault which was encountered. | |
val sequence : | |
reports : Report<'Pass, IFault> array -> Report<'Pass array, IFault> | |
/// <summary> | |
/// Tools for working with <see cref="T:pblasucci.FaultReport.Report`2"/> | |
/// in conjunction with <see cref='T:Microsoft.FSharp.Collections.list`1'/>. | |
/// </summary> | |
[<RequireQualifiedAccess>] | |
module List = | |
/// Splits a collection of reports into two collections: | |
/// one containing only the passing value; | |
/// and, one containing only the failing values. | |
val divide : | |
items : Report<'Pass, 'Fail> list -> 'Pass list * 'Fail list | |
when 'Fail :> IFault | |
/// <summary> | |
/// Applies the given function to each of the given items, | |
/// accumulating either the passing data or the faults into a single | |
/// <see cref="T:pblasucci.FaultReport.Report`2"/> instance. | |
/// </summary> | |
/// <remarks> | |
/// The <c>Report</c> instance returned from this function will only be in | |
/// a passing state if all the input produced passing values (when the | |
/// given input function is applied to each item). Further, all items of the | |
/// given input collection will always be iterated (ie: all possible failures | |
/// will be collected). | |
/// </remarks> | |
val accumulate : | |
project : ('T -> Report<'Pass, IFault>) -> | |
items : 'T list -> | |
Report<'Pass list, CompoundFault> | |
/// <summary> | |
/// Applies the given function to each of the given items, | |
/// accumulating either the passing data or the first fault into a single | |
/// <see cref="T:pblasucci.FaultReport.Report`2"/> instance. | |
/// </summary> | |
/// <remarks> | |
/// The <c>Report</c> instance returned from this function will only be in | |
/// a passing state if all the input produced passing values (when the | |
/// given input function is applied to each item). Further, all items of the | |
/// given input collection may not be iterated (ie: processing will stop after | |
/// the first failing value is detected). | |
/// </remarks> | |
val traverse : | |
project : ('T -> Report<'Pass, IFault>) -> | |
items : 'T list -> | |
Report<'Pass list, IFault> | |
/// Turns a collection of report instances into a single report containing either: | |
/// each of the passing values; or, the first fault which was encountered. | |
val sequence : | |
reports : Report<'Pass, IFault> list -> Report<'Pass list, IFault> | |
/// <summary> | |
/// Tools for working with <see cref="T:pblasucci.FaultReport.Report`2"/> | |
/// in conjunction with <see cref='T:Microsoft.FSharp.Collections.seq`1'/>. | |
/// </summary> | |
[<RequireQualifiedAccess>] | |
module Seq = | |
/// Splits a collection of reports into two collections: | |
/// one containing only the passing value; | |
/// and, one containing only the failing values. | |
val divide : | |
items : Report<'Pass, 'Fail> seq -> 'Pass seq * 'Fail seq | |
when 'Fail :> IFault | |
/// <summary> | |
/// Applies the given function to each of the given items, | |
/// accumulating either the passing data or the faults into a single | |
/// <see cref="T:pblasucci.FaultReport.Report`2"/> instance. | |
/// </summary> | |
/// <remarks> | |
/// The <c>Report</c> instance returned from this function will only be in | |
/// a passing state if all the input produced passing values (when the | |
/// given input function is applied to each item). Further, all items of the | |
/// given input collection will always be iterated (ie: all possible failures | |
/// will be collected). | |
/// </remarks> | |
val accumulate : | |
project : ('T -> Report<'Pass, IFault>) -> | |
items : 'T seq -> | |
Report<'Pass seq, CompoundFault> | |
/// <summary> | |
/// Applies the given function to each of the given items, | |
/// accumulating either the passing data or the first fault into a single | |
/// <see cref="T:pblasucci.FaultReport.Report`2"/> instance. | |
/// </summary> | |
/// <remarks> | |
/// The <c>Report</c> instance returned from this function will only be in | |
/// a passing state if all the input produced passing values (when the | |
/// given input function is applied to each item). Further, all items of the | |
/// given input collection may not be iterated (ie: processing will stop after | |
/// the first failing value is detected). | |
/// </remarks> | |
val traverse : | |
project : ('T -> Report<'Pass, IFault>) -> | |
items : 'T seq -> | |
Report<'Pass seq, IFault> | |
/// Turns a collection of report instances into a single report containing either: | |
/// each of the passing values; or, the first fault which was encountered. | |
val sequence : | |
reports : Report<'Pass, IFault> seq -> Report<'Pass seq, IFault> |
Interesting take. The one feature I think is missing is the stack trace. That would complicate the construction, but I think it's a pretty important bit missed out on most functional error handling concepts.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
FaultReport
This sketches out, moreorless, what I wish had been added to FSharp.Core instead of
Result<'T, 'TError>
. Basically, it's a very similar type, but with some structure and constraint around how failures are represented.Main Concepts
IFault
... a simple contract which spells out the basic requirements for modeling a failure.Report<'Pass, 'Fail>
... a type analogous toResult<'T, 'TError>
, except'Fail
is constrained to implementIFault
.Report<_,_>
exactly the same as you would withResult<_, _>
.Report<_, IFault>
.Files
BareMinimum.fs
is the absolute least amount of code one can reasonable expect and still keep the most important benefits.FaultReport.fsi
/FaultReport.fs
is the expanded set of types and utilities, which more fully realizes the approach.The main benefits this approach has (over
Result<_, _>
) are:string
orexn
) cannot be used to represent failures.Report<_, _>
instances with different failure representations (like from different libraries) can be combined without excessive re-mapping.IFault.Cause
property, and theCompoundFault
type, complex error models are possible (albeit uncommon).However, it's worth noting, none of the important functionality of
Result<_,_>
is sacrificed. Specifically:IFault
type, pattern matching on specific failure types is still possible (via the|FailAs|
active pattern).NB: Support for consumption from C# has not been factored into this work. If it had been, some of the API surface would be different (most notably, the variants on
Report<_, _>
would beprivate
and shadowed by a total multicase active pattern).