Skip to content

Instantly share code, notes, and snippets.

@isaacabraham
Last active December 23, 2024 16:51
Show Gist options
  • Save isaacabraham/3314300b3758cfca23df30e8dc3ca814 to your computer and use it in GitHub Desktop.
Save isaacabraham/3314300b3758cfca23df30e8dc3ca814 to your computer and use it in GitHub Desktop.
Experimenting with Paul Blasucci's ideas around Faults and Reports for F#, using the existing F# Result type.
#if INTERACTIVE
#r "nuget: FsToolkit.ErrorHandling"
#endif
open System.Text.Json
open FsToolkit.ErrorHandling
/// All Error DUs should implement this to "take part" in generalised fault reporting.
[<Interface>]
type IFault =
abstract Description : string
/// An alias over Result that constrains the error type to some implementation of IFault.
type Report<'TOk, 'TError when 'TError :> IFault> = Result<'TOk, 'TError>
[<AutoOpen>]
module Report =
let generalise report =
report |> Result.mapError (fun error -> error :> IFault)
/// Just for fun
let (!!) = generalise
/// If you want to get to Description without upcasting, use this generalised pattenr
let (|GeneralError|GeneralOk|) (x:Report<'Pass, 'TError> when 'TError :> IFault) =
match x with
| Ok x -> GeneralOk x
| Error a -> GeneralError (a :> IFault)
// Two example error types that are not unified
type ValidationError =
| RequiredFieldMissing of string
| InvalidField of field:string * error:string
interface IFault with
member this.Description: string =
match this with
| RequiredFieldMissing missing -> $"Required field {missing} missing"
| InvalidField(field, error) -> $"Invalid field {field}: {error}"
type CustomerError =
| CustomerNotFound of customerId:int
| CustomerAlreadyExists of customerId:int
interface IFault with
member this.Description: string =
match this with
| CustomerNotFound customerId -> $"Customer not found: {customerId}"
| CustomerAlreadyExists customerId -> $"Customer already exists: {customerId}"
// Two example functions using those error types
let validateRequest (request : {| Id : string | null |}) : Report<int,_> = result {
let! customerId = request.Id |> Option.ofObj |> Result.requireSome (RequiredFieldMissing "ID")
return! customerId |> Option.tryParse |> Result.requireSome (InvalidField("ID", "Not a number"))
}
let loadCustomer customerId : Report<{| CustomerId : int; Name : string |},_> =
match customerId with
| 123 -> Ok {| CustomerId = 123; Name = "Alice" |}
| _ -> Error (CustomerNotFound customerId)
/// A top-level function that brings it all together
let handleRequest request =
let customerId = !! (validateRequest request) // using hardcore operator to go from custom error to IFault... going wild
// could also just do this of course! ;)
// let customerId = request |> validateRequest |> generalise
let customer = customerId |> Result.bind (loadCustomer >> generalise) // using more basicerer function composition to do the same here
// Here's the code that wants to treat all errors in the same way
match customer with
| Error error -> JsonSerializer.Serialize {| Error = error.Description |}
| Ok v -> JsonSerializer.Serialize v // HTTP 20x response
let x = handleRequest {| Id = "123" |}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment