Last active
May 17, 2024 20:32
-
-
Save williamcotton/95ebc21965e393f4c99a597b0c6f36cf to your computer and use it in GitHub Desktop.
A ResultWriter Computation Expression in F#
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
// Define types | |
type Result<'T> = | |
| Success of 'T | |
| Error of string | |
type LogState = { Logs: string list } | |
type ResultLog<'T> = Result<'T> * LogState | |
// Define computation expression | |
type ResultWriterBuilder() = | |
member _.Bind((result, state: LogState), f) = | |
match result with | |
| Success value -> | |
let (newResult, newState) = f value | |
(newResult, { state with Logs = state.Logs @ newState.Logs }) | |
| Error e -> | |
(Error e, state) | |
member _.Return(value) = | |
(Success value, { Logs = [] }) | |
let resultWriter = ResultWriterBuilder() | |
// Define functions | |
let multiply10 x = | |
if x > 0 then (Success (x * 10), { Logs = [sprintf "multiply10 successful: %d" (x * 10)] }) | |
else (Error "x must be positive", { Logs = ["multiply10 failed: x must be positive"] }) | |
let add1 x = | |
if x < 100 then (Success (x + 1), { Logs = [sprintf "add1 successful: %d" (x + 1)] }) | |
else (Error "result too large", { Logs = ["add1 failed: x too large"] }) | |
// Run computation expressions | |
let num1 = resultWriter { | |
let! a = multiply10 5 | |
let! b = add1 a | |
return b | |
} | |
match num1 with | |
| Success finalResult, logs -> | |
printfn "Final result: %d" finalResult | |
printfn "Logs: %A" logs.Logs | |
| Error e, logs -> | |
printfn "An error occurred: %s" e | |
printfn "Logs: %A" logs.Logs | |
// Final result: 51 | |
// Logs: ["multiply10 successful: 50"; "add1 successful: 51"] | |
let num2 = resultWriter { | |
let! a = multiply10 50 | |
let! b = add1 a | |
let! c = multiply10 b | |
return c | |
} | |
match num2 with | |
| Success finalResult, logs -> | |
printfn "Final result: %d" finalResult | |
printfn "Logs: %A" logs.Logs | |
| Error e, logs -> | |
printfn "An error occurred: %s" e | |
printfn "Logs: %A" logs.Logs | |
// An error occurred: result too large | |
// Logs: ["multiply10 successful: 500"; "add1 failed: x too large"] |
Now that I'm thinking at the type-level:
// Define types
type ResultWriter<'T, 'E, 'L> = Result<'T, 'E> * 'L
// Define computation expression
type ResultWriterBuilder() =
member _.Bind((result, log), f) =
match result with
| Success value ->
let (newResult, newLog) = f value
(newResult, log @ newLog)
| Error e ->
(Error e, log)
member _.Return(value) =
(Success value, [])
let resultWriter = ResultWriterBuilder()
// Define functions
let multiply10 x =
if x > 0 then
let product = x * 10
(Success product, [sprintf "multiply10 successful: %d" product])
else (Error "x must be positive", ["multiply10 failed: x must be positive"])
let add1 x =
if x < 100 then
let sum = x + 1
(Success sum, [sprintf "add1 successful: %d" sum])
else (Error "result too large", ["add1 failed: x too large"])
// Run computation expressions
let num1 = resultWriter {
let! a = multiply10 5
let! b = add1 a
return b
}
match num1 with
| Success finalResult, log ->
printfn "Final result: %d" finalResult
printfn "Logs: %A" log
| Error e, logs ->
printfn "An error occurred: %s" e
printfn "Logs: %A" log
// Final result: 51
// Log: ["multiply10 successful: 50"; "add1 successful: 51"]
let num2 = resultWriter {
let! a = multiply10 50
let! b = add1 a
let! c = multiply10 b
return c
}
match num2 with
| Success finalResult, log ->
printfn "Final result: %d" finalResult
printfn "Log: %A" log
| Error e, log ->
printfn "An error occurred: %s" e
printfn "Log: %A" log
// An error occurred: result too large
// Log: ["multiply10 successful: 500"; "add1 failed: x too large"]
Depends how generic you want to go, really. For example, in tests you do want to build up a list of logs, but in prod you really don't (it's pretty inefficient!). You could be generic over this behaviour (for example), but honestly at that point I think you're probably better just injecting an ILogger
. (Remember that debugging through a computation expression is usually a bad experience; I have a pretty strong bias at this point against using them for anything other than async
.)
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Oooh, yes, works perfectly and is much more reusable! Thank you!