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
\$\endgroup\$
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.

\$\endgroup\$
  • \$\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\$ – Peter Siebke Apr 22 '16 at 14:19

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.