2
\$\begingroup\$

This code represents logic used to retry failed calls to a crypto exchange:

let retries =
    [
        TimeSpan.FromSeconds(1.)
        TimeSpan.FromSeconds(3.)
        TimeSpan.FromSeconds(5.)
        TimeSpan.FromSeconds(10.)
        TimeSpan.FromSeconds(30.)
    ]


let rec retryAsync (retryAttempts: TimeSpan list) (request: unit -> Async<Result<'a, ExchangeError>>) : Async<Result<'a, ExchangeError>> =
    async {
        let! r = request()
        match r with
        | Result.Ok x ->
            return Result.Ok x
        | Result.Error e ->
            match e with
            | ExchangeError.HttpError _
            | ExchangeError.ServiceUnavailable
            | ExchangeError.InvalidTimestamp ->
                match retryAttempts with
                | [] ->
                    return Result.Error e
                | h::t ->
                    Thread.Sleep(h)
                    return! retryAsync t request
            | _ ->
                return Result.Error e
    }

The logic is as follow: There are a bunch of functions that return Async<Result<'a, ExchangeError>> and perform various external operations.

I am wrapping them in this code where results that return ok will be passed through, and results that return error will go through some more scrutiny. Some errors trigger a retry (HttpError, ServiceUnavailable and InvalidTimestamp) since they are transient errors which are not linked to the request itself. Other errors are not going through a retry and are passed through directly.

I would like to know if this could be expressed in a more concise / "understandable right away by a 3rd party" way.

It can be used that way:

// cancel orders
let cancelOrdersAsync (exchange: IExchange) instrument (orderIds: string list) : Async<Result<unit, ExchangeError>> =
    exchange.CancelOrdersAsync(instrument, orderIds)

// cancel orders and retry
let cancelOrdersRetryAsync (exchange: IExchange) instrument (orderIds: string list) =
    retryAsync retries (fun _ -> cancelOrdersAsync exchange instrument orderIds)

as an extra question: could this be made as a curried function where I'd just have something like retryAsync retries cancelOrders

\$\endgroup\$
1
  • 1
    \$\begingroup\$ I don't have enough to add to make this a full answer, but as a beginner to f# I find this code easily understandable without your explanation. If your goal is to make this understandable to someone who doesn't know even basic f#, then I would issue a frame challenge to say that's not really a desirable goal. \$\endgroup\$
    – Turksarama
    Jun 22, 2021 at 23:05

1 Answer 1

2
\$\begingroup\$

It looks understandable to me, here's a couple possible improvements:

retryInterval is more informative than retries (you could also specify that these are the defaults):

let defaultRetryIntervals =
    [
        TimeSpan.FromSeconds(1.)
        TimeSpan.FromSeconds(3.)
        TimeSpan.FromSeconds(5.)
        TimeSpan.FromSeconds(10.)
        TimeSpan.FromSeconds(30.)
    ]

retryAsync could be made a little more general, while also clarifying what exactly it's meant to handle (just the retry logic) - we can also reduce a match nesting level:

let rec retry<'a>
    (shouldRetry: 'a -> bool)
    (retryIntervals: TimeSpan list)
    (request: unit -> Async<'a>)
        : Async<'a>
    =
    async {
        let! result = request()
        match shouldRetry result, retryIntervals with
        | true, head::rest ->
            Thread.Sleep(head)
            return! retry shouldRetry rest request
        | false, _
        | _, [] ->
            return result
    }

And the equivalent to your retryAsync becomes:

let shouldRetryExchange =
    function
    | Result.Error (ExchangeError.HttpError _)
    | Result.Error ExchangeError.ServiceUnavailable
    | Result.Error ExchangeError.InvalidTimestamp ->
        true
    | _ ->
        false

let retryExchange<'ok> = retry<Result<'ok,_>> shouldRetryExchange

as an extra question: could this be made as a curried function where I'd just have something like retryAsync retries cancelOrders

Here's an answer from Tomas Petricek on that. You'd need to create (tupled) single-argument versions of your api functions:

let cancelOrdersAsync'
    (
        exchange: IExchange,
        instrument,
        orderIds: string list
    ) =
    cancelOrdersAsync exchange instrument orderIds

and then modify retry to get a curried cancelOrdersRetryAsync:

let rec retryArg<'a, 'arg>
    (shouldRetry: 'a -> bool)
    (retryIntervals: TimeSpan list)
    (request: 'arg -> Async<'a>)
    arg
        : Async<'a>
    =
    async {
        let! result = request arg
        ... same as before ...
    }

let cancelOrdersRetryAsync =
    retryArg shouldRetryExchange defaultRetryIntervals cancelOrdersAsync'
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.