Skip to content

Instantly share code, notes, and snippets.

@williamcotton
Last active May 17, 2024 20:32
Show Gist options
  • Save williamcotton/95ebc21965e393f4c99a597b0c6f36cf to your computer and use it in GitHub Desktop.
Save williamcotton/95ebc21965e393f4c99a597b0c6f36cf to your computer and use it in GitHub Desktop.
A ResultWriter Computation Expression in F#
// 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"]
@Smaug123
Copy link

Personally if I wanted to specialise the error type to string I'd just use Result from FSharp.Core directly, rather than defining your own Result<'a> := Result<'a, string>. That is, delete the definition of Result and instead use type ResultLog<'T> = Result<'T, string> * LogState.

But in fact nothing you've written here requires the error type to be string, so instead just define type ResultLog<'T, 'E> = Result<'T, 'E> * LogState and everything should go through.

@williamcotton
Copy link
Author

Oooh, yes, works perfectly and is much more reusable! Thank you!

@williamcotton
Copy link
Author

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"]

@Smaug123
Copy link

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