in F #, quando si desidera gestire recuperabili errori quasi universalmente desidera utilizzare il o il tipo option
Choice<_,_>
. In pratica l'unica differenza tra loro è che Choice
consente di restituire alcune informazioni sull'errore mentre option
no. In altre parole, option
è la cosa migliore quando non è importante in che modo o perché un errore non è riuscito (solo che ha avuto esito negativo); Choice<_,_>
viene utilizzato quando si dispone di informazioni su come o perché il problema di non è riuscito. Ad esempio, potresti voler scrivere le informazioni di errore in un log; o forse vuoi gestire una situazione di errore in modo diverso in base a perché il problema di non è riuscito: un ottimo caso per questo è fornire messaggi di errore precisi per aiutare gli utenti a diagnosticare un problema.
Con questo in mente, ecco come mi piacerebbe refactoring del codice per gestire gli errori in uno stile funzionale, pulito:
open System
open System.Net
/// Retrieves the content at the given URI.
let retrievePage (client : WebClient) (uri : Uri) =
// Preconditions
checkNonNull "uri" uri
if not <| uri.IsAbsoluteUri then
invalidArg "uri" "The URI must be an absolute URI."
try
// If the data is retrieved successfully, return it.
client.DownloadData uri
|> Choice1Of2
with
| :? System.Net.WebException as webExn ->
// Return the URI and WebException so they can be used to diagnose the problem.
Choice2Of2 (uri, webExn)
| _ ->
// Reraise any other exceptions -- we don't want to handle them here.
reraise()
/// Retrieves the content at the given URI.
/// If a WebException is raised when retrieving the content, the request
/// will be retried up to a specified number of times.
let rec retrievePageRetry (retryWaitTime : TimeSpan) remainingRetries (client : WebClient) (uri : Uri) =
// Preconditions
checkNonNull "uri" uri
if not <| uri.IsAbsoluteUri then
invalidArg "uri" "The URI must be an absolute URI."
elif remainingRetries = 0u then
invalidArg "remainingRetries" "The number of retries must be greater than zero (0)."
// Try to retrieve the page.
match retrievePage client uri with
| Choice1Of2 _ as result ->
// Successfully retrieved the page. Return the result.
result
| Choice2Of2 _ as error ->
// Decrement the number of retries.
let retries = remainingRetries - 1u
// If there are no retries left, return the error along with the URI
// for diagnostic purposes; otherwise, wait a bit and try again.
if retries = 0u then error
else
// NOTE : If this is modified to use 'async', you MUST
// change this to use 'Async.Sleep' here instead!
System.Threading.Thread.Sleep retryWaitTime
// Try retrieving the page again.
retrievePageRetry retryWaitTime retries client uri
[<EntryPoint>]
let main argv =
/// WebClient used for retrieving content.
use wc = new WebClient()
/// The amount of time to wait before re-attempting to fetch a page.
let retryWaitTime = TimeSpan.FromSeconds 2.0
/// The maximum number of times we'll try to fetch each page.
let maxPageRetries = 3u
/// The URI to fetch.
let fullURI = Uri ("http://www.badaddress.xyz", UriKind.Absolute)
// Fetch the page data.
match retrievePageRetry retryWaitTime maxPageRetries wc fullURI with
| Choice1Of2 pageData ->
printfn "Retrieved %u bytes from: %O" (Array.length pageData) fullURI
0 // Success
| Choice2Of2 (uri, error) ->
printfn "Unable to retrieve the content from: %O" uri
printfn "HTTP Status: (%i) %O" (int error.Status) error.Status
printfn "Message: %s" error.Message
1 // Failure
Fondamentalmente, ho diviso il codice fuori in due funzioni, più l'originale main
:
- Una funzione che tenta di recuperare il contenuto da un URI specificato.
- Una funzione che contiene la logica per tentativi di riprovare; questo "avvolge" la prima funzione che esegue le richieste effettive.
- La funzione principale originale ora gestisce solo le "impostazioni" (che è possibile estrarre facilmente da un
app.config
o web.config
) e stampare i risultati finali. In altre parole, è ignaro della logica di riprogrammazione: è possibile modificare la singola riga di codice con l'istruzione match
e utilizzare la funzione di richiesta non ripetuta, se lo si desidera.
Se si vuole estrarre il contenuto da più URI e attendere una notevole quantità di tempo (ad esempio, 5 minuti) tra i tentativi, è necessario modificare la logica di ritentare di utilizzare una coda di priorità o qualcosa invece di utilizzare Thread.Sleep
o Async.Sleep
.
Plug vergogna: la mia libreria ExtCore contiene alcune cose per rendere la vita molto più semplice quando si costruisce qualcosa di simile, soprattutto se si desidera rendere tutto asincrono. Ancora più importante, fornisce un flusso di lavoro asyncChoice
e collections functions designed to work with it.
Per quanto riguarda la domanda relativa al passaggio dei parametri (come il numero di tentativi e il numero di tentativi), non penso ci sia una regola ferrea per decidere se passarli o codificarli all'interno la funzione. Nella maggior parte dei casi, preferisco passarli, ma se hai più di alcuni parametri da passare, è meglio creare un record per tenerli tutti e passare quello. Un altro approccio che ho usato è quello di creare i valori option
, dove i valori di default sono estratti da un file di configurazione (anche se vorrai estrarli dal file una volta e assegnarli ad un campo privato per evitare ri-analisi il file di configurazione ogni volta che viene chiamata la funzione); questo facilita la modifica dei valori predefiniti che hai usato nel tuo codice, ma ti dà anche la possibilità di sovrascriverli quando necessario.