Repository F# setup
open Reified
open Reified.Refinements
open Reified.Result
open Reified.Schema.JsonObserving a Result
tap and tapError run a side effect and hand the result back unchanged. They exist so logging does not force you to
break a pipeline apart.
open System
open Reified.Result
type SignupError =
| AgeNotANumber of string
| AgeOutOfRange of int
SystemReifiedResult03-result-handling_60-observing.md_page.SignupErrorAgeNotANumberstringAn abbreviation for the CLI type . Basic Types
AgeOutOfRangeintAn abbreviation for the CLI type . Basic Types
Without them
Logging mid-pipeline means naming the intermediate value and returning it again:
let logged =
let outcome = parseAge raw
match outcome with
| Ok age -> printfn "accepted %d" age
| Error failure -> printfn "rejected: %A" failure
outcomeWith them
parseAge "abc"
|> Result.tap (fun age -> printfn "accepted %d" age)
|> Result.tapError (fun failure -> printfn "rejected: %A" failure)rejected: AgeNotANumber "abc"
The value returned is the original Error (AgeNotANumber "abc"). tap did not run, because the result was not Ok;
tapError ran and returned its input untouched.
Both signatures say the same thing — the effect returns unit, so it has no way to influence what comes out:
Result.tap : ('value -> unit) -> Result<'value, 'error> -> Result<'value, 'error>
Result.tapError : ('error -> unit) -> Result<'value, 'error> -> Result<'value, 'error>Where they earn their place
At a boundary, where you want a record of what happened but the caller still gets the untouched result:
let handleSignup raw =
parseAge raw
|> Result.tapError (fun failure -> logger.Warning("signup rejected: {Failure}", failure))
|> Result.map buildAccountKeep the effect small and total. An effect that throws will propagate out of tap, which defeats the purpose of
working in Result — and because the exception escapes mid-pipeline, the result you were carrying is lost.