Consider the following function that takes a Sequence of (pairs) int * int
.
let f s =Essentially I am looking for the first pair in the sequence where the second element in the pair satisfies a particular condition and then returning the corresponding first element or a default (i.e. 10) if no satisfactory pair was found.
let o = Seq.tryFind (fun (_, x) -> x < 0) s
if Option.isSome o then
Option.get o |> fst
else
let p = Seq.tryFind (fun (_, x) -> x = 0) s
if Option.isSome p then Option.get p |> fst
else 10
That code is pretty ordinary. First of all, I would like to improve on the two calls to
Seq.tryFind
.let f s =Now if the Scala Option
let tf g = Seq.tryFind (fun (_,x) -> g x) s
let o = tf (fun x -> x < 0)
if Option.isSome o then
Option.get o |> fst
else
let p = tf ((=) 0)
if Option.isSome p then Option.get p |> fst
else 10
orElse
and getOrElse
functions were available as well as the Haskell backquotes infix syntax then we could really make a difference.Neither
orElse : 'T option -> 'T option -> 'T option
let orElse o p = if Option.isSome o then o else p
getOrElse : 'T option -> 'T -> 'T
let getOrElse o d = match o with | Some x -> x | _ -> d
// N.B. this is not valid F#
let f s =
let tf g = Seq.tryFind (fun (_,x) -> g x) s
tf (fun x -> x < 0) `orElse` tf ((=) 0) |> Option.map fst `getOrElse` 10
orElse
or getOrElse
exist in the F# Option
module. However the Core function defaultArg
is essentially getOrElse
.Unfortunately F# doesn't have a way to use a (non-operator) function in infix form, like the backquotes in Haskell. However, we can define operators, which are essentially just infix functions.
Now we can write a valid F# version.
let (|?) = orElse
let (|?|) = defaultArg
let f s =
let tf g = Seq.tryFind (fun (_,x) -> g x) s
tf (fun x -> x < 0) |? tf ((=) 0) |> Option.map fst |?| 10
Laziness and Composability
Notice that in the original example, the second
tryFind
is only executed if the first one is unsuccessful because the then
expression of an if
statement is lazy, i.e. it is only evaluated if the condition is false.However functions in F# are strict by default i.e. their arguments are evaluated prior to application. Consequently both
tryFind
s are evaluated, irrespective of their values, as they are arguments to orElse
.So here is an example where laziness is required to achieve reasonable composability. Implementing this gives the final code.
let orElse o (p:'a option Lazy) = if Option.isSome o then o else p.Force()
let (|?) = orElse
let (|?|) = defaultArg
let f s =
let tf g = Seq.tryFind (fun (_,x) -> g x) s
tf (fun x -> x < 0) |? lazy (tf ((=) 0)) |> Option.map fst |?| 10
1 comment:
Old stuff, I know, but I was intrigued by the topic. Introducing explicit laziness does not sound like a performance enhancement to me, and I also cannot find it more readable than the original code, although that is also somewhat weird. Anything wrong with the following code? (Looks like the spacing is incorrect because the blog filters out initial space.)
let f s =
let inline tf g = Seq.tryFind (fun (_, x) -> g x) s
match tf (( > ) 0) with
| Some (a, _) -> a
| None ->
match tf (( = ) 0) with
| Some (b, _) -> b
| None -> 10
Post a Comment