Created
September 2, 2025 01:38
-
-
Save 1eyewonder/1c7a7c745c02295498c7a1d1ec9e3bf9 to your computer and use it in GitHub Desktop.
playground for "stack traces" in CEs
This file contains hidden or 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
#r "nuget: FsToolkit.ErrorHandling" | |
open FsToolkit.ErrorHandling | |
open System.Runtime.CompilerServices | |
open System | |
open System.IO | |
#nowarn 3535 | |
[<RequireQualifiedAccess>] | |
module Seq = | |
let inline cons x xs = seq { | |
yield x | |
yield! xs | |
} | |
type Yarn = { | |
CallerMemberName: string | |
ExecutingMemberName: string | |
CallerFilePath: string | |
CallerLineNumber: int | |
} with | |
static member Create(caller: _ option, executing: _ option, path: _ option, line: _ option) = { | |
CallerMemberName = defaultArg caller "" | |
ExecutingMemberName = defaultArg executing "" | |
CallerFilePath = defaultArg path "" | |
CallerLineNumber = defaultArg line 0 | |
} | |
let inline formatCallerMember (t: Yarn) = | |
$"Executing %s{t.ExecutingMemberName} from %s{t.CallerMemberName} in %s{t.CallerFilePath}:line %i{t.CallerLineNumber}" | |
type YarnNode = { | |
Yarn: Yarn | |
Children: YarnNode list | |
Depth: int | |
} | |
module YarnTree = | |
let buildTree (yarns: Yarn seq) = | |
let yarnList = yarns |> Seq.toList | |
let rec buildNodeTree (remaining: Yarn list) (currentDepth: int) (acc: YarnNode list) = | |
match remaining with | |
| [] -> List.rev acc | |
| yarn :: rest -> | |
let isNewBranch = | |
acc | |
|> List.tryHead | |
|> Option.map (fun prev -> prev.Yarn.CallerMemberName <> yarn.CallerMemberName) | |
|> Option.defaultValue true | |
let depth = if isNewBranch then currentDepth else currentDepth + 1 | |
let node = { Yarn = yarn; Children = []; Depth = depth } | |
buildNodeTree rest depth (node :: acc) | |
buildNodeTree yarnList 0 [] | |
let pruneTree (yarnFilters: seq<Yarn -> Yarn option>) (nodes: YarnNode list) = | |
let rec pruneNode (node: YarnNode) = | |
let prunedChildren = | |
node.Children | |
|> List.choose pruneNode | |
let filteredYarn = | |
yarnFilters | |
|> Seq.fold (fun yarnOpt filter -> | |
match yarnOpt with | |
| Some y -> filter y | |
| None -> None | |
) (Some node.Yarn) | |
match filteredYarn with | |
| Some yarn -> Some { node with Yarn = yarn; Children = prunedChildren } | |
| None -> None | |
nodes |> List.choose pruneNode | |
let rec formatTree (nodes: YarnNode list) = | |
nodes | |
|> List.map (fun node -> | |
let indent = String.replicate (node.Depth * 2) " " | |
let formatted = formatCallerMember node.Yarn | |
$"%s{indent}├─ %s{formatted}") | |
|> String.concat Environment.NewLine | |
module Yarn = | |
module FilterOut = | |
let callerMember (value: string) (t: Yarn) = | |
if t.CallerMemberName.Equals(value, StringComparison.OrdinalIgnoreCase) then | |
None | |
else | |
Some t | |
let executingMember (value: string) (t: Yarn) = | |
if String.Equals(t.ExecutingMemberName, value, StringComparison.OrdinalIgnoreCase) then | |
None | |
else | |
Some t | |
let callerFilePath (value: string) (t: Yarn) = | |
if t.CallerFilePath.Equals(value, StringComparison.OrdinalIgnoreCase) then | |
None | |
else | |
Some t | |
let callerLineNumber value (t: Yarn) = | |
if t.CallerLineNumber = value then None else Some t | |
module WriteOutput = | |
let simple (ball: Yarn seq) = | |
ball |> Seq.map formatCallerMember |> String.concat Environment.NewLine | |
let debuggable blacklist (ball: Yarn seq) = | |
YarnTree.buildTree ball | |
|> YarnTree.pruneTree blacklist | |
|> YarnTree.formatTree | |
type IUnravel<'e> = | |
static abstract Unravel: Yarn * 'e -> 'e | |
// ======================== | |
// TO SAVE YOURSELF FROM SCROLLING YOU LIKELY WANT | |
// TO FOLD THIS UNLESS YOU ARE INSPECTING THE CE CODE | |
// ======================= | |
[<AutoOpen>] | |
module ComputationExpressions = | |
type ResultTraceBuilder() = | |
member inline _.Return<'ok, 'error when IUnravel<'error>>(value: 'ok) : Result<'ok, 'error> = Ok value | |
member inline this.ReturnFrom<'ok, 'error when IUnravel<'error>> | |
( | |
result: Result<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<'ok, 'error> = | |
match result with | |
| Ok v -> Ok v | |
| Error e -> | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.ReturnFrom), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
) | |
member this.Zero<'ok, 'error when IUnravel<'error>>() : Result<unit, 'error> = this.Return() | |
member inline this.Bind<'okInput, 'okOutput, 'error when IUnravel<'error>> | |
( | |
input: Result<'okInput, 'error>, | |
[<InlineIfLambda>] binder: 'okInput -> Result<'okOutput, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<'okOutput, 'error> = | |
match input with | |
| Ok v -> | |
match binder v with | |
| Ok o -> Ok o | |
| Error e -> | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Bind), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
) | |
| Error e -> | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Bind), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
) | |
member inline this.Delay<'ok, 'error when IUnravel<'error>> | |
( | |
[<InlineIfLambda>] generator: unit -> Result<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : unit -> Result<'ok, 'error> = | |
fun () -> | |
match generator () with | |
| Ok v -> Ok v | |
| Error e -> | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Delay), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
) | |
member inline this.Run<'ok, 'error when IUnravel<'error>> | |
( | |
[<InlineIfLambda>] generator: unit -> Result<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<'ok, 'error> = | |
match generator () with | |
| Ok v -> Ok v | |
| Error e -> | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Run), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
) | |
member inline this.Combine<'ok, 'error when IUnravel<'error>> | |
(result: Result<unit, 'error>, [<InlineIfLambda>] binder: unit -> Result<'ok, 'error>) | |
: Result<'ok, 'error> = | |
this.Bind(result, binder) | |
member inline this.TryWith<'T, 'TError when IUnravel<'TError>> | |
( | |
[<InlineIfLambda>] generator: unit -> Result<'T, 'TError>, | |
[<InlineIfLambda>] handler: exn -> Result<'T, 'TError>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<'T, 'TError> = | |
try | |
this.Run generator | |
with e -> | |
match handler e with | |
| Ok v -> Ok v | |
| Error err -> | |
Error( | |
'TError | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.TryWith), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
err | |
) | |
) | |
member inline this.TryFinally<'ok, 'error when IUnravel<'error>> | |
( | |
[<InlineIfLambda>] generator: unit -> Result<'ok, 'error>, | |
[<InlineIfLambda>] compensation: unit -> unit, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<'ok, 'error> = | |
try | |
this.Run generator | |
finally | |
compensation () | |
member inline this.Using<'error when IUnravel<'error>> | |
( | |
resource: 'disposable :> IDisposableNull, | |
binder: 'disposable -> Result<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<'ok, 'error> = | |
this.TryFinally( | |
(fun () -> | |
match binder resource with | |
| Ok v -> Ok v | |
| Error e -> | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Using), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
)), | |
(fun () -> | |
if not (obj.ReferenceEquals(resource, null)) then | |
resource.Dispose()) | |
) | |
member inline this.While<'ok, 'error when IUnravel<'error>> | |
( | |
[<InlineIfLambda>] guard: unit -> bool, | |
[<InlineIfLambda>] generator: unit -> Result<unit, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<unit, 'error> = | |
let mutable doContinue = true | |
let mutable result = Ok() | |
while doContinue && guard () do | |
match generator () with | |
| Ok() -> () | |
| Error e -> | |
doContinue <- false | |
result <- | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.While), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
) | |
result | |
member inline this.For<'TError when IUnravel<'TError>> | |
( | |
sequence: #seq<'T>, | |
[<InlineIfLambda>] binder: 'T -> Result<unit, 'TError>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<unit, 'TError> = | |
this.Using( | |
sequence.GetEnumerator(), | |
fun enum -> | |
this.While( | |
(fun () -> enum.MoveNext()), | |
this.Delay(fun () -> | |
match binder enum.Current with | |
| Ok() -> Ok() | |
| Error e -> | |
Error( | |
'TError | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.For), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
)) | |
) | |
) | |
member inline this.BindReturn<'okInput, 'okOutput, 'error when IUnravel<'error>> | |
( | |
x: Result<'okInput, 'error>, | |
[<InlineIfLambda>] f: 'okInput -> 'okOutput, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<'okOutput, 'error> = | |
match x with | |
| Ok v -> Ok(f v) | |
| Error e -> | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.BindReturn), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
) | |
/// <summary> | |
/// Method lets us transform data types into our internal representation. This is the identity method to recognize the self type. | |
/// | |
/// See https://stackoverflow.com/questions/35286541/why-would-you-use-builder-source-in-a-custom-computation-expression-builder | |
/// </summary> | |
/// <param name="result"></param> | |
/// <returns></returns> | |
member inline this.Source<'ok, 'error when IUnravel<'error>> | |
( | |
result: Result<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Result<'ok, 'error> = | |
match result with | |
| Ok v -> Ok v | |
| Error e -> | |
Error( | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Source), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
) | |
) | |
/// <summary> | |
/// The <c>Result</c> computation expression. | |
/// </summary> | |
let resultTrace = ResultTraceBuilder() | |
type ValidationTraceBuilder() = | |
member inline _.Return(value: 'ok) : Validation<'ok, 'error> = Validation.ok value | |
member inline this.ReturnFrom<'ok, 'error when IUnravel<'error>> | |
( | |
result: Validation<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'ok, 'error> = | |
match result with | |
| Ok v -> Ok v | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.ReturnFrom), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
member inline this.Bind<'okInput, 'okOutput, 'error when IUnravel<'error>> | |
( | |
result: Validation<'okInput, 'error>, | |
[<InlineIfLambda>] binder: 'okInput -> Validation<'okOutput, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'okOutput, 'error> = | |
match result with | |
| Ok v -> | |
match binder v with | |
| Ok o -> Ok o | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Bind), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Bind), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
member inline this.Zero() : Validation<unit, 'error> = this.Return() | |
member inline this.Delay<'ok, 'error when IUnravel<'error>> | |
( | |
[<InlineIfLambda>] generator: unit -> Validation<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : unit -> Validation<'ok, 'error> = | |
fun () -> | |
match generator () with | |
| Ok v -> Ok v | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Delay), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
member inline this.Run<'ok, 'error when IUnravel<'error>> | |
( | |
[<InlineIfLambda>] generator: unit -> Validation<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'ok, 'error> = | |
match generator () with | |
| Ok v -> Ok v | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Run), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
member inline this.Combine<'ok, 'error when IUnravel<'error>> | |
(result: Validation<unit, 'error>, [<InlineIfLambda>] binder: unit -> Validation<'ok, 'error>) | |
: Validation<'ok, 'error> = | |
this.Bind(result, binder) | |
member inline this.TryWith | |
( | |
[<InlineIfLambda>] generator: unit -> Validation<'ok, 'error>, | |
[<InlineIfLambda>] handler: exn -> Validation<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'ok, 'error> = | |
try | |
this.Run generator | |
with e -> | |
match handler e with | |
| Ok v -> Ok v | |
| Error errors -> | |
errors | |
|> List.map (fun err -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.TryWith), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
err | |
)) | |
|> Error | |
member inline this.TryFinally<'ok, 'error when IUnravel<'error>> | |
([<InlineIfLambda>] generator: unit -> Validation<'ok, 'error>, [<InlineIfLambda>] compensation: unit -> unit) | |
: Validation<'ok, 'error> = | |
try | |
this.Run generator | |
finally | |
compensation () | |
member inline this.Using | |
( | |
resource: 'disposable :> IDisposableNull, | |
[<InlineIfLambda>] binder: 'disposable -> Validation<'okOutput, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'okOutput, 'error> = | |
this.TryFinally( | |
(fun () -> | |
match binder resource with | |
| Ok v -> Ok v | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Using), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error), | |
(fun () -> | |
if not (obj.ReferenceEquals(resource, null)) then | |
resource.Dispose()) | |
) | |
member inline this.While<'ok, 'error when IUnravel<'error>> | |
( | |
[<InlineIfLambda>] guard: unit -> bool, | |
[<InlineIfLambda>] generator: unit -> Validation<unit, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<unit, 'error> = | |
let mutable doContinue = true | |
let mutable result = Ok() | |
while doContinue && guard () do | |
match generator () with | |
| Ok() -> () | |
| Error errors -> | |
doContinue <- false | |
result <- | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.While), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
result | |
member inline this.For<'ok, 'error when IUnravel<'error>> | |
( | |
sequence: #seq<'ok>, | |
[<InlineIfLambda>] binder: 'ok -> Validation<unit, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<unit, 'error> = | |
this.Using( | |
sequence.GetEnumerator(), | |
fun enum -> | |
this.While( | |
enum.MoveNext, | |
this.Delay(fun () -> | |
match binder enum.Current with | |
| Ok() -> Ok() | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.For), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error) | |
) | |
) | |
member inline this.BindReturn<'okInput, 'okOutput, 'error when IUnravel<'error>> | |
( | |
input: Validation<'okInput, 'error>, | |
[<InlineIfLambda>] mapper: 'okInput -> 'okOutput, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'okOutput, 'error> = | |
match input with | |
| Ok v -> Ok(mapper v) | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.BindReturn), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
member inline this.MergeSources<'left, 'right, 'error when IUnravel<'error>> | |
( | |
left: Validation<'left, 'error>, | |
right: Validation<'right, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'left * 'right, 'error> = | |
match Validation.zip left right with | |
| Ok(l, r) -> Ok(l, r) | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.MergeSources), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
/// <summary> | |
/// Method lets us transform data types into our internal representation. This is the identity method to recognize the self type. | |
/// | |
/// See https://stackoverflow.com/questions/35286541/why-would-you-use-builder-source-in-a-custom-computation-expression-builder | |
/// </summary> | |
/// <param name="result"></param> | |
/// <returns></returns> | |
member inline this.Source<'ok, 'error when IUnravel<'error>> | |
( | |
result: Validation<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'ok, 'error> = | |
match result with | |
| Ok v -> Ok v | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (nameof ValidationTraceBuilder) (nameof this.Source), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
let validationTrace = ValidationTraceBuilder() | |
[<AutoOpen>] | |
module ValidationTraceCEExtensions = | |
// Having members as extensions gives them lower priority in | |
// overload resolution and allows skipping more type annotations. | |
type ValidationTraceBuilder with | |
/// <summary> | |
/// Needed to allow `for..in` and `for..do` functionality | |
/// </summary> | |
member inline _.Source(s: #seq<_>) : #seq<_> = s | |
/// <summary> | |
/// Method lets us transform data types into our internal representation. | |
/// </summary> | |
member inline this.Source<'ok, 'error when IUnravel<'error>> | |
( | |
s: Result<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'ok, 'error> = | |
match Validation.ofResult s with | |
| Ok v -> Ok v | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Source), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
/// <summary> | |
/// Method lets us transform data types into our internal representation. | |
/// </summary> | |
/// <returns></returns> | |
member inline this.Source<'ok, 'error when IUnravel<'error>> | |
( | |
choice: Choice<'ok, 'error>, | |
[<CallerMemberName>] ?memberName: string, | |
[<CallerFilePath>] ?sourceFilePath: string, | |
[<CallerLineNumber>] ?sourceLineNumber: int | |
) : Validation<'ok, 'error> = | |
match Validation.ofChoice choice with | |
| Ok v -> Ok v | |
| Error errors -> | |
errors | |
|> List.map (fun e -> | |
'error | |
.Unravel( | |
Yarn.Create( | |
memberName, | |
Some <| sprintf "%s.%s" (this.GetType().FullName) (nameof this.Source), | |
sourceFilePath, | |
sourceLineNumber | |
), | |
e | |
)) | |
|> Error | |
// ======================== | |
// Example stuff starts here | |
// ======================== | |
type IBedError<'e> = | |
static abstract iTooCold: int -> 'e | |
static abstract iAlarmFailed: 'e | |
let getOutOfBed<'e when IBedError<'e>> (d: DateTime) = | |
if d.Hour < 6 then Error('e.iTooCold 5) | |
elif d.Hour > 9 then Error 'e.iAlarmFailed | |
else Ok() | |
let doFoo () = resultTrace { | |
let! x = getOutOfBed <| DateTime.Parse("2021-01-01 10:00:00") | |
return () | |
} | |
let doFoo2 () = resultTrace { | |
let! x = getOutOfBed <| DateTime.Parse("2021-01-01 5:00:00") | |
return () | |
} | |
let doBar () = resultTrace { | |
let! x = doFoo () | |
return () | |
} | |
let doBaz () = resultTrace { | |
let! x = doBar () | |
return () | |
} | |
let doQux () = resultTrace { | |
let! x = doBaz () | |
return () | |
} | |
let doQux2 () = resultTrace { return! doFoo2 () } | |
let ultraQux () = validationTrace { | |
let! x = doQux () | |
and! y = doQux2 () | |
return 2 | |
} | |
type FooErrors = | |
| TooCold of int * Yarn seq | |
| AlarmFailed of Yarn seq | |
interface IBedError<FooErrors> with | |
static member iTooCold e = TooCold(e, Seq.empty) | |
static member iAlarmFailed = AlarmFailed Seq.empty | |
interface IUnravel<FooErrors> with | |
static member Unravel(msg, e) = | |
match e with | |
| TooCold(e, ball) -> TooCold(e, Seq.cons msg ball) | |
| AlarmFailed ball -> Seq.cons msg ball |> AlarmFailed | |
// simple output of the stack trace | |
member x.FormattedTrace blacklist = | |
match x with | |
| TooCold(_, ball) | |
| AlarmFailed ball -> | |
ball | |
|> Seq.filter (fun yarn -> | |
blacklist | |
|> Seq.forall (fun f -> f yarn |> Option.isSome) | |
) | |
|> Yarn.WriteOutput.simple | |
// nicely formatted tree output of the stack trace | |
member x.DebuggingTrace blacklist = | |
match x with | |
| TooCold(_, ball) | |
| AlarmFailed ball -> | |
YarnTree.buildTree ball | |
|> YarnTree.pruneTree blacklist | |
|> YarnTree.formatTree | |
// can configure how detailed the stacks traces are | |
let yarnIgnores = | |
seq { | |
// ignore these CE member calls. They are likely overkill and not needed, | |
// but it is nice to see the inner workings of the CE for example purposes | |
Yarn.FilterOut.executingMember "FSI_0004+ComputationExpressions+ResultTraceBuilder.Run" | |
Yarn.FilterOut.executingMember "FSI_0004+ComputationExpressions+ResultTraceBuilder.Delay" | |
Yarn.FilterOut.executingMember "FSI_0004+ComputationExpressions+ResultTraceBuilder.Source" | |
Yarn.FilterOut.executingMember "FSI_0004+ComputationExpressions+ValidationTraceBuilder.Run" | |
Yarn.FilterOut.executingMember "FSI_0004+ComputationExpressions+ValidationTraceBuilder.Delay" | |
Yarn.FilterOut.executingMember "FSI_0004+ComputationExpressions+ValidationTraceBuilder.Source" | |
Yarn.FilterOut.executingMember "FSI_0004+ComputationExpressions+ValidationTraceBuilder.MergeSources" | |
// Yarn.FilterOut.callerMember "doBaz" | |
// ignore specific lines in specific files | |
// yield! | |
// seq { | |
// // @"c/Users/brons/source/repos/Unravel/test.fsx", 821 | |
// } | |
// |> Seq.map (fun (path, line) -> | |
// Yarn.FilterOut.callerFilePath path | |
// >> Option.bind (Yarn.FilterOut.callerLineNumber line) | |
// ) | |
} | |
|> Seq.cache | |
match ultraQux () with | |
| Ok _ -> printfn "Success" | |
| Error(errors: FooErrors list) -> | |
errors | |
|> Seq.iter (fun e -> printfn $"%s{e.FormattedTrace yarnIgnores}") | |
printfn "-----" | |
errors | |
|> Seq.iter (fun e -> printfn $"%s{e.DebuggingTrace yarnIgnores}") | |
printfn "-----" | |
printfn "%A" errors |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment