Как закодировать логику "повторить" (только с 1 итерацией) в F # без изменчивых варов?

Итак, пытаясь избежать изменчивых переменных, я придумал следующий код повторной логики, который кажется уродливым:

let result = TryConnect()
match result with
| ErrorConnecting ->
    SetupConnectionParameters()
    let resultAgain = TryConnect()
    match resultAgain with
    | ErrorConnecting ->
                      Console.Error.WriteLine("Setup failed!")
                      Environment.Exit(7)
    | Success(value) -> PerformOperations(value)
| Success(value) -> PerformOperations(value)

Есть ли способ уменьшить некоторое дублирование здесь? (Помните, нет mutable vars.) Спасибо!

Ответы

Ответ 1

Поскольку здесь представлено много альтернатив, вот еще один:

let private tryConnectNth n = 
    if n <> 0 then SetupConnectionParameters()
    TryConnect()

let isSuccess = function
    |Success _ -> true
    |ErrorConnecting -> false

let tryConnect n =
    Seq.init n tryConnectNth // generate a sequence of n connection attempts
    |> Seq.tryFind isSuccess // try to find the first success - returns Option
    |> Option.fold (fun _ -> id) ErrorConnecting // return ErrorConnecting if None, or result otherwise

Он вызывает SetupConnectionParameters() только при попытке ненулевого подключения и повторяется до n раз.

Ответ 2

Сделайте функцию рекурсивной с параметром для повторений:

let rec private tryToConnectAux tryAgain =
    match TryConnect() with
    | Success(value) -> PerformOperations(value)
    | ErrorConnecting when tryAgain ->
        SetupConnectionParameters ()
        tryToConnectAux false
    | ErrorConnecting ->
        Console.Error.WriteLine("Setup failed!")
        Environment.Exit(7)

Вызов через tryToConnectAux true.


Этот ответ был отредактирован. Исходный код:

let rec tryConnecting nRetries =
    match TryConnect() with
    | ErrorConnecting ->
        if nRetries > 0 then tryConnecting (nRetries - 1)
        else
            Console.Error.WriteLine("Setup failed!")
            Environment.Exit(7)
    | Success(value) -> PerformOperations(value)

(Эта версия не включает SetupConnectionParameters(), вы должны добавить ее в любое место)

Ответ 3

Вы можете разделить логику повтора на отдельную функцию. Вот пример с большим количеством печати на консоль, чтобы проиллюстрировать, что происходит.

let rec retry f tries =
    printfn "Trying..."
    match f () with
    | Some successValue ->
        printfn "Success"
        Some successValue
    | None ->
        match tries with
        | [] ->
            printfn "Failed"
            None
        | delayMs :: rest ->
            printfn "Waiting %i ms..." delayMs
            System.Threading.Thread.Sleep(delayMs:int)
            retry f rest

let random = System.Random()

let connect () =
    if random.Next(100) < 30 then Some "connection"
    else None

match retry connect [100; 100] with
| Some connection -> printfn "Do something with connection."
| None -> printfn "Could not connect."

Попробуйте выполнить последнее выражение несколько раз.

  • Это дает вам гибкое количество попыток с дополнительной задержкой после каждого (количество предоставленных задержек - количество попыток).

  • Необходимо использовать код для использования функции retry. Вам нужно сделать функцию, которая пытается подключиться один раз и возвращает соединение, завернутое в Some, если оно выполнено успешно, или просто None, если оно не выполнено. Затем передайте эту функцию в качестве параметра f.

Ответ 4

В то время как я ценю, что @Vandroiy пытается, его блок не точно ведет себя как мой исходный код (потому что я намеренно не хочу называть SetupConnectionParameters() в первый раз).

Это мой результат, вдохновленный его ответом и первоначальным намеком Джона:

let rec TryConnectAndMaybeSetup(retries) =
    if (retries > 1) then
        Console.Error.WriteLine("Setup failed")
        Environment.Exit(7)

    let result = TryConnect()
    match result with
    | ErrorConnecting ->
        SetupConnectionParameters()
        TryConnectAndMaybeSetup(retries + 1)
    | Success(value) -> PerformOperations(value)

TryConnectAndMaybeSetup(0)

Эта альтернатива также проще, чем @TheQuickBrownFox's.

Ответ 5

Здесь другое решение, основанное на решении Vandroiy, которое вызывает только функцию настройки при первом сбое.

let tryConnecting = 
    let rec connect nRetries setupFunction =
        match TryConnect() with
        | ErrorConnecting ->
            if nRetries > 0 then 
                setupFunction()
                connect (nRetries - 1) setupFunction
            else
                Console.Error.WriteLine("Setup failed!")
                Environment.Exit(7)
        | Success(value) -> PerformOperations(value)
    connect 1 SetupConnectionParameters

Ответ 6

Вот итерационное решение, основанное на функции Seq.unfold. Мы используем эту функцию для генерации ленивой последовательности событий Success/Failed. Затем мы можем выполнить манипуляции в этой последовательности, чтобы получить успешный результат, или остановимся после нескольких попыток.

Сначала определим сигнатуру функции, которая может выйти из строя:

type ActionResult<'a> = 
    | Success of 'a
    | ErrorConnecting

type getValue<'a> = unit -> ActionResult<'a>

Затем определите дискриминируемый союз, который моделирует все различные состояния, в которых мы могли бы быть в отношении повторения:

type Retry<'a> = 
    | Success of 'a * int
    | Failure of int
    | Untried

Теперь, учитывая результат последней попытки, мы сгенерируем следующий элемент в последовательности:

let unfolder (functionInvoke : getValue<_>) (retryParameters : Retry<_>) : ((Retry<_>* Retry<_>) option) = 

    let nextRetryResult () = 
        match functionInvoke() with
        | ActionResult.ErrorConnecting -> 
            match retryParameters with
            | Untried -> Failure 1
            | Failure pastRetries -> Failure (pastRetries + 1)
        | ActionResult.Success value -> 
            match retryParameters with
            | Untried -> Success (value, 0 )
            | Failure pastRetries -> Success (value, pastRetries )

    match retryParameters with
        | Untried 
        | Failure _ -> Some(retryParameters, nextRetryResult() )
        | success -> Some(retryParameters, success)

Теперь мы можем использовать эту функцию для создания функции getResultWithRetries:

let isNotSuccessAndLimitNotReached limit (retry : Retry<'a>) = 
    match retry with
    | Untried -> true
    | Failure retryCount when retryCount < limit -> true
    | _ -> false

let getResultWithRetries limit getValue   = 
    Seq.unfold (unfolder getValue) Retry.Untried 
    |> Seq.skipWhile(isNotSuccessAndLimitNotReached limit)
    |> Seq.head

Наконец, мы можем проверить это:

let successValue = getResultWithRetries 3 (fun () -> ActionResult.Success "ABC")
let ``fail after 3 attempts`` : Retry<string> = getResultWithRetries 3 (fun () -> ActionResult.ErrorConnecting)
let ``fail after 5 attempts`` : Retry<string> = getResultWithRetries 5 (fun () -> ActionResult.ErrorConnecting)

Используя следующую функцию, мы можем проверить, что происходит с нечистыми функциями:

let succeedOn count = 
  let mutable callCount = 0
  let f () = 
    match callCount < count with
    | true -> 
      callCount <- callCount + 1
      ErrorConnecting
    | false -> ActionResult.Success "ABC"
  f


let ``result after 3 attempts when succeeds on 2nd`` : Retry<string> = getResultWithRetries 3 (succeedOn 2)
let ``result after 3 attempts when succeeds on 5th`` : Retry<string> = getResultWithRetries 3 (succeedOn 5)