如何使用 Option.map 和 Option.bind 重写多个空检查?

How to use Option.map and Option.bind to rewrite the multiple null checks?

如何将以下使用c# HtmlAgility库的代码转换为优雅风格?

if node <> null then
  let nodes = node.SelectNodes("//input[@name='xxx']")
  if nodes <> null then
    let first = nodes.[0]
    if first <> null then
      let value = first.Attributes.["value"]
        if value <> null then
          Some value.Value
        else
          None
     else
       None
   else
     None
else
  None

下面的代码可以工作吗?但是,它仍然不如 C# 6 的 ?. 运算符简洁。

let toOpt = function null -> None | x -> Some x
node |> toOpt
|> Option.map (fun x -> x.SelectNodes("//input[@name='xxx']"))
|> Option.map (fun x -> x.[0]                                ) 
|> Option.map (fun x -> x.Attributes.["value"]               ) 
|> Option.map (fun x -> x.Value                              ) 

C#6版本还是简洁多了:

node?.SelectNodes("//input[@name='xxx']")[0]?.Attributes["value"]?.Value

Option.bind 有帮助吗?

FYI F#4 添加了 Option.ofObj

在 F# 中,null 被避免是有充分理由的。在处理依赖于 null 的 C# 库时,我的一般建议是在该库上提供 F# 惯用的 "adapter"。

在实践中,这可能需要大量工作,而且结果可能不像 C# 运算符 ?. 那样简洁(暂且不讨论这样的运算符是否是个好主意)。

据我所知,F# 编译器不支持这样的运算符,但如果您强烈支持它,您应该在 http://fslang.uservoice.com/ 提出它。 F# 社区很友好,但我怀疑您将不得不非常激烈地争论才能让社区相信这对 F# 来说是个好主意。

同时;使它稍微更简洁的一种方法是创建一个像这样的 computation expressiongetAttributeValue 是您的代码的样子):

// Basically like the classic `maybe` monad
//  but with added support for nullable types
module Opt =

  let inline Return v : Option<'T> = Some v

  let inline ReturnFrom t : Option<'T> = t
  let inline ReturnFrom_Nullable ot : Option<'T> =
    match ot with
    | null -> None
    | _ -> Some ot

  let inline Bind (ot : Option<'T>) (fu : 'T -> Option<'U>) : Option<'U> =
    match ot with
    | None -> None
    | Some vt -> 
      let ou = fu vt
      ou

  let inline Bind_Nullable (vt : 'T) (fu : 'T -> Option<'U>) : Option<'U> =
    match vt with
    | null -> None
    | _ -> 
      let ou = fu vt
      ou

  let Delay ft : Option<'T> = ft ()

  type OptBuilder() =
    member inline x.Return v       = Return v
    member inline x.ReturnFrom v   = ReturnFrom v
    member inline x.ReturnFrom v   = ReturnFrom_Nullable v
    member inline x.Bind (t, fu)   = Bind t fu
    member inline x.Bind (t, fu)   = Bind_Nullable t fu
    member inline x.Delay ft       = Delay ft

let inline ofObj o =
  match o with
  | null -> None
  | _ -> Some o

open HtmlAgilityPack

let opt = Opt.OptBuilder()

let getAttributeValue (node : HtmlNode) (path : string) : string option =
  opt {
    let! nodes  = node.SelectNodes path
    let! node   = nodes.[0]
    let! attr   = node.Attributes.["value"] 
    return! attr.Value
  }


let html = """
<html>
  <title>Hello</title>
  <body>Yellow <div name='Test' value='Stone'>Div</div></title>
</html>
"""

[<EntryPoint>]
let main argv = 
  let doc = HtmlDocument ()
  doc.LoadHtml html
  let r = getAttributeValue doc.DocumentNode "//div[@name='Test']"
  printfn "Result: %A" r
  0

您可以在 Fsharpx 中使用 maybe monad

maybe {
  let! node = toOpt node
  let! nodes = toOpt node.SelectNodes("")
  let! first = toOpt nodes.[0]
  let! value = toOpt first.Attributes.["value"]
  return value.Value
}

如果任何一个为空,这将导致 None,否则将导致 Some value.Value

注意 如果你通读一遍,FuleSnabel 的解决方案实际上更好,因为它可以让你摆脱所有地方的 toOpt,你可以拥有它就是

opt {
  let! node = node
  let! nodes = node.SelectNodes("")
  let! first = nodes.[0]
  let! value = first.Attributes.["value"]
  return value.Value
}

选择这个而不是那个的唯一原因是,如果您真的只想将您的项目限制在 Fsharpx 中定义的标准工作流构建器,而不是定义您自己的自定义构建器(您可以复制和粘贴)来自那个答案)。