【问题标题】:F# computation expression transparent state passing with Bind使用 Bind 传递 F# 计算表达式透明状态
【发布时间】:2015-06-13 12:53:56
【问题描述】:

我有以下代码尝试使用通常的 MaybeBuilder 从网络流中读取可能不完整的数据(例如图像数据):

let image = maybe {
    let pos = 2 //Initial position skips 2 bytes of packet ID
    let! width, pos = readStreamAsInt 2 pos
    let! height, pos = readStreamAsInt 2 pos
    let! data, pos = readStream (width*height) pos
    advanceInStream pos
    return {width = width; height = height; pixels = data}
}

因此,如果数据尚未到达 NetworkStream,readStream[asInt] [numBytes] [offset] 函数将返回 Some [data] 或 None。 AdvanceInStream函数在读取整个网络数据包时执行。

我想知道是否有某种方法可以编写一些自定义计算表达式构建器来隐藏从其用户传递的 pos,因为它总是相同的 - 我在流中读取一些数据和位置并将其作为最后一个传递给下一个读取函数参数。

附:使用的 MaybeBuilder:

type MaybeBuilder() =    
    member x.Bind(d,f) = Option.bind f d
    member x.Return d = Some d
    member x.ReturnFrom d = d
    member x.Zero() = None
let maybe = new MaybeBuilder()

P.P.S

转念一想,我似乎必须使 pos 可变,因为阅读中可能出现“for”或“while”循环。简单让!使用 pos Bind 阴影效果很好,但是如果你在循环中添加阅读,你就不能保持不变性,对吧?那么任务就变得微不足道了。

【问题讨论】:

  • 是的,这确实可以写成计算表达式。这在定义解析器计算表达式时很常见(确实需要跟踪字符串中的位置)。
  • 有两个问题需要解决:(1) 使用pos 似乎是State 计算表达式的有效工作,而(2) 使用返回Option<'T> 的函数是Maybe comp.expression 的工作,和你一样。最大的问题是F#中的计算表达式组合不好,例如,你可能有一个或另一个,但为了同时得到两个,你需要编写自己的自定义comp。将做这两件事的表达式。出于学习目的,这很好,但在现实生活中的项目中,这些可能看起来很难支持。

标签: f# computation-expression


【解决方案1】:

@bytebuster 在自定义计算表达式的可维护性方面做得很好,但我仍然认为我演示了如何将 StateMaybe monad 组合成一个。

在“传统”语言中,我们对组合值(例如整数)有很好的支持,但是在开发解析器时会遇到问题(从二进制流中生成值本质上就是解析)。对于解析器,我们希望将简单的解析器函数组合成更复杂的解析器函数,但这里的“传统”语言通常缺乏良好的支持。

在函数式语言中,函数和值一样普通,因为值可以组合,显然函数也可以。

首先让我们定义一个StreamReader 函数。 StreamReader 接受 StreamPosition(流 + 位置)并生成更新后的 StreamPositionStreamReaderResult(读取值或失败)。

type StreamReader<'T> = 
  StreamReader of (StreamPosition -> StreamPosition*StreamReaderResult<'T>)

(这是最重要的一步。)

我们希望能够将简单的StreamReader 函数组合成更复杂的函数。我们要维护的一个非常重要的属性是,组合操作在StreamReader 下是“关闭”的,这意味着组合的结果是一个新的StreamReader,而它又可以无休止地组合。

为了读取图像,我们需要读取宽度和高度、计算乘积并读取字节。像这样的:

let readImage = 
  reader {
    let! width  = readInt32 
    let! height = readInt32 
    let! bytes  = readBytes (width*height)

    return width, height, bytes
  }

因为组合被关闭readImageStreamReader&lt;int*int*byte[]&gt;

为了能够像上面那样组合StreamReader,我们需要定义一个计算表达式,但在我们这样做之前,我们需要为StreamReader定义操作ReturnBind。事实证明Yield 也很好。

module StreamReader =
  let Return v : StreamReader<'T> =
    StreamReader <| fun sp -> 
      sp, (Success v)

  let Bind (StreamReader t) (fu : 'T -> StreamReader<'U>) : StreamReader<'U> =
    StreamReader <| fun sp -> 
      let tsp, tr = t sp
      match tr with
      | Success tv ->
        let (StreamReader u) = fu tv
        u tsp
      | Failure tfs -> tsp, Failure tfs

  let Yield (ft : unit -> StreamReader<'T>) : StreamReader<'T> =
    StreamReader <| fun sp -> 
      let (StreamReader t) = ft ()
      t sp

Return 是微不足道的,因为 StreamReader 应该返回给定值并且不要更新 StreamPosition

Bind 更具挑战性,但描述了如何将两个 StreamReader 函数组合成一个新函数。 Bind 运行第一个 StreamReader 函数并检查结果,如果失败则返回失败,否则使用 StreamReader 结果计算第二个 StreamReader 并在更新流位置上运行。

Yield 只是创建StreamReader 函数并运行它。 F# 在构建计算表达式时使用Yield

最后让我们创建计算表达式构建器

type StreamReaderBuilder() =
  member x.Return v   = StreamReader.Return v
  member x.Bind(t,fu) = StreamReader.Bind t fu
  member x.Yield(ft)  = StreamReader.Yield ft

let reader = StreamReaderBuilder ()

现在我们构建了组合StreamReader 函数的基本框架。此外,我们还需要定义基本的StreamReader 函数。

完整示例:

open System
open System.IO

// The result of a stream reader operation is either
//  Success of value
//  Failure of list of failures
type StreamReaderResult<'T> =
  | Success of 'T
  | Failure of (string*StreamPosition) list

and StreamPosition =
  {
    Stream    : byte[]
    Position  : int
  }

  member x.Remaining = max 0 (x.Stream.Length - x.Position)

  member x.ReadBytes (size : int) : StreamPosition*StreamReaderResult<byte[]> =
    if x.Remaining < size then
      x, Failure ["EOS", x]
    else
      let nsp = StreamPosition.New x.Stream (x.Position + size)
      nsp, Success (x.Stream.[x.Position..(x.Position + size - 1)])

  member x.Read (converter : byte[]*int -> 'T) : StreamPosition*StreamReaderResult<'T> =
    let size = sizeof<'T>
    if x.Remaining < size then
      x, Failure ["EOS", x]
    else
      let nsp = StreamPosition.New x.Stream (x.Position + size)
      nsp, Success (converter (x.Stream, x.Position))

  static member New s p = {Stream = s; Position = p;}

// Defining the StreamReader<'T> function is the most important decision
//   In this case a stream reader is a function that takes a StreamPosition 
//   and produces a (potentially) new StreamPosition and a StreamReadeResult
type StreamReader<'T> = StreamReader of (StreamPosition -> StreamPosition*StreamReaderResult<'T>)

// Defining the StreamReader CE
module StreamReader =
  let Return v : StreamReader<'T> =
    StreamReader <| fun sp -> 
      sp, (Success v)

  let Bind (StreamReader t) (fu : 'T -> StreamReader<'U>) : StreamReader<'U> =
    StreamReader <| fun sp -> 
      let tsp, tr = t sp
      match tr with
      | Success tv ->
        let (StreamReader u) = fu tv
        u tsp
      | Failure tfs -> tsp, Failure tfs

  let Yield (ft : unit -> StreamReader<'T>) : StreamReader<'T> =
    StreamReader <| fun sp -> 
      let (StreamReader t) = ft ()
      t sp

type StreamReaderBuilder() =
  member x.Return v   = StreamReader.Return v
  member x.Bind(t,fu) = StreamReader.Bind t fu
  member x.Yield(ft)  = StreamReader.Yield ft

let reader = StreamReaderBuilder ()

let read (StreamReader sr) (bytes : byte[]) (pos : int) : StreamReaderResult<'T> =
  let sp    = StreamPosition.New bytes pos
  let _, sr = sr sp
  sr

// Defining various stream reader functions
let readValue (converter : byte[]*int -> 'T) : StreamReader<'T> =
  StreamReader <| fun sp -> sp.Read converter

let readInt32 = readValue BitConverter.ToInt32
let readInt16 = readValue BitConverter.ToInt16
let readBytes size : StreamReader<byte[]> = 
  StreamReader <| fun sp -> 
    sp.ReadBytes size

let readImage = 
  reader {
    let! width  = readInt32 
    let! height = readInt32 
    let! bytes  = readBytes (width*height)

    return width, height, bytes
  }

[<EntryPoint>]
let main argv = 
  // Sample byte stream
  let bytes   = [|2;0;0;0;3;0;0;0;1;2;3;4;5;6|] |> Array.map byte
  let result  = read readImage bytes 0

  printfn "%A" result

  0

【讨论】:

  • 谢谢,我想我理解了主要思想 - monadic 类型本身不是一个值,而是一个函数状态 -> state * 结果,所以 Bind (以及整个表达式)返回这个函数,它允许沿着链向下传递流位置。看来我在帖子中对“For”循环和可变性有误,我现在会尝试正确编写它。
  • 是的,没错。组合解析器函数非常强大。例如,如果您查看 FParsec(一个解析器库),您会发现它使用了类似的方法。
  • 添加了更多说明。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多