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.