Jon*_*ats 5 f# functional-programming purely-functional
我有以下F#程序从互联网上检索网页:
open System.Net
[<EntryPoint>]
let main argv =
let mutable pageData : byte[] = [| |]
let fullURI = "http://www.badaddress.xyz"
let wc = new WebClient()
try
pageData <- wc.DownloadData(fullURI)
()
with
| :? System.Net.WebException as err -> printfn "Web error: \n%s" err.Message
| exn -> printfn "Unknown exception:\n%s" exn.Message
0 // return an integer exit code
Run Code Online (Sandbox Code Playgroud)
这工作得很好,如果 URI是有效的并且该机拥有互联网连接和 Web服务器等正确响应在一个理想的函数式编程的世界函数的结果将不依赖于外部变量不作为参数(副作用)通过.
我想知道的是什么是适当的F#设计模式来处理可能需要该函数来处理可恢复的外部错误的操作.例如,如果网站关闭,可能需要等待5分钟再试一次.是否应该显式传递重试次数和重试之间的延迟等参数,或者将这些变量嵌入函数中是否可以?
在F#中,当您想要处理可恢复的错误时,您几乎普遍想要使用option
或Choice<_,_>
类型.在实践中,它们之间的唯一区别是Choice
允许您返回有关错误的一些信息,而option
不是.换句话说,option
当事情失败的方式或原因无关紧要时(最好是失败的),这是最好的; Choice<_,_>
当有关于失败的方式或原因的重要信息时使用.例如,您可能希望将错误信息写入日志; 或者您可能希望根据原因以不同方式处理错误情况 失败的事情 - 一个很好的用例是提供准确的错误消息,以帮助用户诊断问题.
考虑到这一点,这就是我如何重构代码以清晰,实用的方式处理故障:
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
Run Code Online (Sandbox Code Playgroud)
基本上,我将您的代码分成两个函数,加上原始函数main
:
app.config
或web.config
)并打印最终结果.换句话说,它没有注意到重试逻辑 - 您可以match
使用语句修改单行代码,如果需要,可以使用非重试请求函数.如果要从多个URI中提取内容并在重试之间等待大量时间(例如,5分钟),则应修改重试逻辑以使用优先级队列或其他内容而不是使用Thread.Sleep
或Async.Sleep
.
无耻的插件:我的ExtCore库包含一些东西,可以在构建这样的东西时让你的生活变得更加轻松,特别是如果你想让它全部异步的话.最重要的是,它提供了一个asyncChoice
工作流程和集合功能,旨在与它一起工作.
至于你传递参数的问题(比如重试超时和重试次数) - 我认为没有一个严格的规则来决定是否在函数中传递或硬编码.在大多数情况下,我更喜欢传递它们,但是如果你传递的参数不止一些,你最好创建一个记录来保存它们并传递它.我使用的另一种方法是创建参数option
值,其中默认值是从配置文件中提取的(尽管你想要从文件中提取它们一次)并将它们分配给某个私有字段,以避免每次调用函数时重新解析配置文件); 这样可以很容易地修改您在代码中使用的默认值,但也可以在必要时灵活地覆盖它们.