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
1 Answer 1
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.
-
\$\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 areSome
. \$\endgroup\$Peter Siebke– Peter Siebke2016年04月22日 14:19:12 +00:00Commented Apr 22, 2016 at 14:19