2016-07-19 11 views
14

Więc próbując uniknąć zmienne zmienny, wpadłem na następujący kod ponawiania-logicznej, która wydaje brzydki:Jak kodować logikę "spróbuj ponownie" (z tylko jedną iteracją) w F # bez zmiennych zmiennych?

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) 

Czy istnieje jakiś sposób na zmniejszenie powielania tutaj? (Pamiętaj, nie mutable vars.) Dzięki!

+7

użyj rekursji - posiada parametr, który jest prawdziwy przy pierwszym podejściu, a fałsz na drugim. –

+2

6 odpowiedzi. Jaką puszkę z robakami otworzyłeś ... :-) – s952163

Odpowiedz

8

Dokonać rekurencyjnej funkcji z parametrem dla prób:

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) 

rozmowy przez tryToConnectAux true.


Ta odpowiedź została zmodyfikowana. Kod oryginalny:

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) 

(Ta wersja nie zawiera SetupConnectionParameters(), trzeba dodać go na cokolwiek lokalizacja jest właściwe)

+0

jeśli zadzwonię do SetupConnectionParameters() przed wywołaniem 'tryConnecting', wtedy nazywam to zawsze. Chcę tylko wywołać SetupConnectionParameters(), jeśli nie powiedzie się raz (jeśli nie powiedzie się dwa razy, wyjść) – ympostor

+1

@ympostor wow, całkiem sporo tutaj. Edytowałem odpowiedź, aby dopasować zachowanie, którego szukasz, i wycinałeś linię przez klauzulę 'when'. – Vandroiy

8

Można oddzielić logikę ponawiania w oddzielnej funkcji. Oto przykład z dużą ilością drukowania na konsolę, aby zilustrować, co się dzieje.

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

Spróbuj uruchomić ostatnie wyrażenie kilka razy.

  • Daje to elastyczną liczbę prób z opcjonalnym opóźnieniem po każdym (liczba podanych opóźnień to liczba ponownych prób).

  • Powinno być możliwe dostosowanie kodu w celu korzystania z funkcji retry. Musisz wykonać funkcję, która próbuje połączyć się raz i zwraca połączenie owinięte w Some, jeśli mu się to uda, lub po prostu None, jeśli się nie powiedzie. Następnie przekaż tę funkcję jako parametr f.

2

Choć doceniam @ try Vandroiy, jego blok nie zachowują się dokładnie jak mój oryginalny kod (bo celowo nie chce zadzwonić SetupConnectionParameters() po raz pierwszy).

To jest mój wynik, inspirowany jego odpowiedź i podpowiedź początkowej Jona:

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) 

Ta alternatywa jest również prostsze niż @ TheQuickBrownFox jest.

+2

Oczywiście najprościej byłoby użyć mutacji i nie ma nic złego w mutacji z ograniczonym zakresem. – TheQuickBrownFox

2

Oto kolejne rozwiązanie oparte na rozwiązaniu Vandroiy, które wywołuje funkcję konfiguracji tylko przy pierwszej awarii.

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 
2

Oto iteracyjny rozwiązanie, oparte na funkcji Seq.unfold. Używamy tej funkcji do generowania leniwych sekwencji zdarzeń Sukces/Nieudane. Następnie możemy wykonać manipulacje na tej sekwencji, aby uzyskać pomyślny wynik lub zatrzymać się po kilku próbach.

Najpierw niech nam określić podpis funkcji, która może zawodzić:

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

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

następnie zdefiniować discrimianted unii które modele wszystkie różne stany moglibyśmy być w odniesieniu do ponawiania:

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

Teraz, biorąc pod uwagę wynik ostatniego ponowienia, generujemy następny element w sekwencji:

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) 

możemy teraz korzystać z tej funkcji, aby utworzyć getResultWithRetries funkcję:

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 

Możemy wreszcie przetestować to:

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) 

Korzystanie następującą funkcję, możemy sprawdzić, co się dzieje z funkcji nieczystych:

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) 
12

Ponieważ istnieje tutaj wiele alternatywnych opcji, tutaj jest jeszcze jedna:

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 

Apeluje SetupConnectionParameters() tylko na próby połączenia niezerowej i powtarza aż do n razy.

Powiązane problemy