在 F# 中使用不可变值循环

While loop with immutable values in F#

我在 F# while-do 循环中使用可变变量,到目前为止它运行良好,但我很想知道是否有办法让它成为纯粹的功能并且只使用不可变数据?

上下文

该程序侦听 Messenger 并处理其消息。消息以块的形式发送,程序的目的是在消息到达时读取它们并附加所有结果,直到收到最后一条消息 (MessageType.End)。
下面的代码是真实程序的简化版本,但捕获了主要逻辑。 (唯一的区别在于 MessageType.StatusMessageType.Failure 的处理和消息正文的解析)。

程序(F# 6.0 的 F# Interactive 版本 12.0.0.0)

您应该能够 运行 Interactive 中的代码,如果您的版本低于 6.0,请进行我在评论中指出的更改。

// ======================================================================================
// Type  
// ======================================================================================
type MessageType =
   | Partial
   | End
   | Status
   | Failure

type Message = {
   Type: MessageType
   Body: string
   ParsedResult: seq<int option>
}

// ======================================================================================
// Simulation of 3 messages (This is just to show an example of messages)
// ======================================================================================
let SimulatedMessages = [|
   {Type = MessageType.Status; Body = "My Status"; ParsedResult = [None] }
   {Type = MessageType.Partial; Body = "Immutability rocks..."; ParsedResult = [Some 1; Some 2; None] }
   {Type = MessageType.End; Body = "... when you know how to handle it"; ParsedResult = [Some 3] }
|]
let mutable SimulatedMessageIndex = 0

// if version < 6.0:
// replace _.NextMessage with this.NextMessage
// replace SimulatedMessages[SimulatedMessageIndex] with SimulatedMessages.[SimulatedMessageIndex]
type Messenger() =
   member _.NextMessage() =
      if SimulatedMessageIndex < 3 then
         let message = SimulatedMessages[SimulatedMessageIndex] 
         SimulatedMessageIndex <- SimulatedMessageIndex + 1
         message
      else failwith "SequenceIndex out of bound"


// ======================================================================================
// I added a field ParsedResult in the Message to simplify, in reality ParseMessage (message: Message)
// has a complex logic that parses the body to return a sequence of <a' option>
// ======================================================================================
let ParseMessage (message: Message) =
   message.ParsedResult

// ======================================================================================
// My infamous imperative algorithm
// ======================================================================================
let ListenMessenger() =
   let mutable result = Seq.empty<int option>
   let mutable isDone = false
   let messenger = Messenger()

   while not isDone do
      let message = messenger.NextMessage()
      match message.Type with 
      | MessageType.Status -> printfn "Log status"
      | MessageType.Partial -> result <- Seq.append result (ParseMessage message)
      | MessageType.End ->
         result <- Seq.append result (ParseMessage message)
         isDone <- true
      | MessageType.Failure -> 
         printfn "Alert failure"
         isDone <- true
   result

ListenMessenger() |> Seq.iter(printfn "%A")

附加信息

每个 Message 包含一个 MessageTypeBody。为了接收下一条消息,我需要调用 NextMessage(),直到收到最后一条 End 类型的消息。 Messenger 总是从发送几个 MessageType.Status 开始,在 Body 中没有有用的信息,然后消息在块 MessageType.Partial 中传递,直到我收到类型为 MessageType.End 的最后一个块. 我添加了一个字段 ParsedResult 以保持示例简单,真正的程序有一个复杂的解析器,它收集我在 Body 和 return 可选类型序列 [=26] 中需要的信息=].
最后,我无法在Messenger或Message结构上做任何改变,我是需要适配的客户端,另一端是服务器不关心我的不变性问题。

挑战

是否可以将命令式代码修改为纯粹不可变的函数式代码? post F# working with while loop 中有一些有用的片段,我知道这只能通过使用 yield! 来实现,但我无法理解这两个 MessageType不要 return 任何 MessageType.StatusMessageType.Failure。 如果 post 太长,我深表歉意,我只是想提供尽可能多的信息来确定问题的范围。

首先,您可以将收集结果的代码和最后 returns 它们替换为序列表达式 returns 所有结果都使用 yield! 这仍然保持命令式循环,但它删除了结果的命令式处理:

let ListenMessengerSeq() = seq {
  let mutable isDone = false
  let messenger = Messenger()
  while not isDone do
     let message = messenger.NextMessage()
     match message.Type with 
     | MessageType.Status -> printfn "Log status"
     | MessageType.Partial -> 
        yield! ParseMessage message
     | MessageType.End ->
        yield! ParseMessage message
        isDone <- true
     | MessageType.Failure -> 
        printfn "Alert failure"
        isDone <- true }

ListenMessengerSeq() |> Seq.iter(printfn "%A")

现在,您可以改用递归序列表达式来删除 while 循环。为此,您定义了一个函数 loop,它仅在计算未完成的情况下调用自身:

let ListenMessengerSeqRec() = 
  let messenger = Messenger()
  let rec loop () = seq {
     let message = messenger.NextMessage()
     match message.Type with 
     | MessageType.Status -> printfn "Log status"
     | MessageType.Partial -> 
        yield! ParseMessage message
        yield! loop ()
     | MessageType.End ->
        yield! ParseMessage message
     | MessageType.Failure -> 
        printfn "Alert failure" }
  loop()

ListenMessengerSeqRec() |> Seq.iter(printfn "%A")

还有很多其他的写法,但我认为这很干净 - 你也可以看到它与你开始的地方相差不远!