我正在尝试通过 MailboxProcessor<'Msg>
类开始使用 F# 中的代理,但我很快意识到我没有正确处理异常。在哈斯克利的世界里,不会有任何例外,因此处理问题的正确方法是简单地将它们作为响应案例提供;因此,代理可以回复如下内容:
type AgentResponse =
| HandledData of string
| InvalidData of string
然后可以调用代理的 .PostAndReply
方法,并获取一个包含消息的InvalidData
,指示数据无效的原因。然而,这不是哈斯克尔,有时会发生例外。因此,如果我这样做:
let agent =
new MailboxProcessor<string * AsyncReplyChannel<AgentResponse>>(fun inbox ->
async {
while true do
let! (msg, replyChannel) = inbox.Receive()
if msg = "die" then failwith "Unknown exception encountered!"
else replyChannel.Reply(HandledData(msg))
})
agent.Start()
agent.PostAndReply (fun rc -> "die", rc)
agent.PostAndReply (fun rc -> "Test", rc)
对agent.PostAndReply
的第二次调用无限期地阻止。当不使用AsyncReplyChannel
因此只调用agent.Post
时,调用不会阻塞,但是一旦代理遇到异常,新消息就会保留在队列中。在任何一种情况下重新启动代理似乎都是不可能的,因为 agent.Start
函数在再次调用时返回一个InvalidOperationException
,并且处理此问题的自然方法似乎是创建一个具有干净状态的新代理,但随后所有排队的消息都将丢失。
除了将代理的整个身体包裹在try..with
中之外,还有什么好方法可以让代理在异常后继续运行?或者,是否建立了有人可以指出我的"标准"处理方式?
你在 Haskell 中确实有例外:尝试在 ghci 中Data.List.head []
....
不幸的是,缺少依赖类型意味着,在Haskell或F#中,我们可以编写没有计算意义的类型正确的代码。
实际上,用 try ... with
块包装 对于处理异常并不是一个坏主意。你不需要包装整个主体,只需要包裹代码的非纯部分。
然后,通常,您返回使用适当的构造函数生成的值。
一个可能的选项是类似于 Tomas Petricek 在这个问题上定义的HandlingMailbox
的帮助程序类型,它反复运行代理的主体:
type ResilientMailbox<'T> private(f:ResilientMailbox<'T> -> Async<unit>) as self =
let event = Event<_>()
let inbox = new MailboxProcessor<_>(fun _inbox ->
let rec loop() = async {
try
return! f self
with e ->
event.Trigger(e)
return! loop()
}
loop())
member __.OnError = event.Publish
member __.Start() = inbox.Start()
member __.Receive() = inbox.Receive()
member __.Post(v:'T) = inbox.Post(v)
static member Start(f) =
let mbox = new ResilientMailbox<_>(f)
mbox.Start()
mbox
这可以像普通MailboxProcessor
一样启动和运行,但如果提供的代理机构抛出异常,则会重新运行。
编辑:更改内部MailboxProcessor
以使用递归函数而不是while true do..
块;当目标函数正常返回时,以前的代码不会停止运行。