2
\$\begingroup\$

I'm still learning from designing with types non strings by Scott Wlaschin. For my project I transform some XML to HTML with xslt. Because every filename can have side effects, they are stored in string option types. If I do the transformation I get an ugly pyramid of Option.map and five times more lines of code compared to the simple version. Is there a shorter way to write that code?

open System.Xml.Xsl
type HtmlPage = HtmlPage of string
type XmlFile = XmlFile of string
type XslFile = XslFile of string
type HtmlTransformer =
 {
 HtmlPage : HtmlPage option
 XmlFile : XmlFile option
 XslFile : XslFile option
 }
let toHtmlNoOption (xmlFn:string) (xsltFn:string) (htmlFn:string) =
 try
 let xslt = new XslCompiledTransform()
 xslt.Load xsltFn
 xslt.Transform (xmlFn,htmlFn) 
 with _-> ()
let toHtml (sc:HtmlTransformer) =
 try
 let xslt = new XslCompiledTransform()
 let xfn = 
 sc.XslFile
 |> Option.map (fun (XslFile xf) -> xf)
 let xmlFn = 
 sc.XmlFile
 |> Option.map (fun (XmlFile xf) -> xf)
 let htmlFn = 
 sc.HtmlPage
 |> Option.map (fun (HtmlPage lp) -> lp)
 xfn
 |> Option.map xslt.Load
 |> Option.map 
 (fun () ->
 htmlFn
 |> Option.map 
 (fun hFn -> 
 xmlFn
 |> Option.map 
 (fun xFn -> xslt.Transform(xFn,hFn)))
 )
 with 
 _ -> None
200_success
146k22 gold badges190 silver badges478 bronze badges
asked Apr 22, 2016 at 7:24
\$\endgroup\$

1 Answer 1

5
\$\begingroup\$

Because every filename can have side effects, they are stored in string option types.

I don't understand what you mean by that, but it's not particularly important for the rest of the question.

Your concern about the structure of the code is valid. Fortunately, you have several options. Let's start gently.

Map3

Option.map is useful if you have a single option value, and you want to transform it into another option value if the input value is a Some case. If you have two option values, you'll need a hypothetical Option.map2 function, and in this case, where you have three option values, you'll need a hypothetical Option.map3 function. These functions are not yet available in the Option module, but are being considered for inclusion in the future.

You can easily write the required function yourself, though:

// ('a -> 'b -> 'c -> 'd) -> 'a option -> 'b option -> 'c option -> 'd option
let optionMap3 f x y z =
 match x, y, z with
 | Some x', Some y', Some z' -> f x' y' z' |> Some
 | _ -> None

This completely generic function would enable you to write an option-based function adapter over your nice toHtmlNoOption function:

let toHtml' (sc : HtmlTransformer) =
 let xfn = sc.XslFile |> Option.map (fun (XslFile xf) -> xf)
 let xmlFn = sc.XmlFile |> Option.map (fun (XmlFile xf) -> xf)
 let htmlFn = sc.HtmlPage|> Option.map (fun (HtmlPage lp) -> lp)
 (xfn, xmlFn, htmlFn) |||> optionMap3 toHtmlNoOption

The return expression uses the exotic |||> pipe operator to compose the three option values into toHtmlNoOption, which will only be invoked if all three values are Some cases.

Computation expression

You can do better, though, using a computation expression. There's no built-in computation expression for option types, but you can easily add a minimal one that addresses this particular purpose:

type OptionBuilder () =
 member this.Bind(v, f) = Option.bind f v
 member this.Return v = Some v
let option = OptionBuilder ()

This enables you to rewrite the function like this:

let toHtml'' (sc : HtmlTransformer) = option {
 let! XslFile xfn = sc.XslFile 
 let! XmlFile xmlFn = sc.XmlFile
 let! HtmlPage htmlFn = sc.HtmlPage
 return toHtmlNoOption xfn xmlFn htmlFn }

Notice how the use of let!-bound values enable you to use pattern matching to destructure the string values out of the single-union cases. Within this option expression, xfn, xmlFn, and htmlFn all have the type string.

Because this expression is evaluated within an option computation expression, it'll short-circuit and return None as soon as any of the let!-bound values turn out to be None. Only if they are all Some cases does it evaluate all the way to the end, where it calls into the toHtmlNoOption function and returns the return value of that function call.

answered Apr 22, 2016 at 13:08
\$\endgroup\$
1
  • \$\begingroup\$ Thank you very much for the answer that really helped me. So now I will look at operators to learn more about the exotic operators. I started thinking of Computation Expressions after I wrote the code, but I’m still learning them, so your advice helped me. To be moreprecise: The filenames themselves can not have side effects, but creating and accessing them. Names of missing files are None and others are Some. \$\endgroup\$ Commented Apr 22, 2016 at 14:19

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.