Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

f#

How to convert the following code, which uses c# HtmlAgility library, to an elegant style?

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

Could the following code may work? However, it still not as concise as C# 6's ?. operator.

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 version is still much more concise:

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

Can Option.bind helps?

like image 704
ca9163d9 Avatar asked May 01 '15 06:05

ca9163d9


1 Answers

FYI F#4 has added Option.ofObj

In F# null is avoided for good reasons. When dealing with C# libraries that rely on null my general advice would be to provide an F# idiomatic "adapter" on that library.

In practice that might be quite much work and the result might not be as succinct as the C# operator ?. (leaving aside the argument whether such an operator is a good idea or not).

To my knowledge the F# compiler doesn't support such an operator but if you feel strongly for it you should raise it at: http://fslang.uservoice.com/ . The F# community is friendly but I suspect you will have to argue quite vigorously in order to convince the community that it's a good idea for F#.

Meanwhile; one way to make it slightly more succinct is to create a computation expression like this (getAttributeValue is what your code will look like):

// 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
like image 161
Just another metaprogrammer Avatar answered Oct 07 '22 18:10

Just another metaprogrammer