So, I have made this function called tryMap which is as follows:
/// tryMap, with failure and success continuations.
let rec tryMapC : 'R -> ('U list -> 'R) -> ('T -> 'U option) -> ('T list) -> 'R =
fun failure success mapping list ->
match list with
| [] -> success []
| head::tail ->
match mapping head with
| None -> failure
| Some result ->
let success = (fun results -> result::results |> success)
tryMapC failure success mapping tail
/// <summary>
/// Attempts to map a list with a partial function.
/// <para/>
/// If a value which maps to None is encountered,
/// the mapping stops, and returns None.
/// <para/>
/// Else, Some(list), containing the mapped values, is returned.
/// </summary>
let tryMap : ('T -> 'U option) -> 'T list -> 'U list option =
fun mapping list ->
tryMapC None Some mapping list
The purpose, as described in its documentation, is to map a list using a partial function, and short curcuit if a "full" mapping isn't "possible", for lack of better words.
Here's an example of how it could be used:
Given this function...
let tryFac n =
do printf "The factorial of %d" n
if n < 0 then
do printf " cannot be computed.\n"
None
else
let result = (List.fold (*) 1 [1..n])
do printf " is %d\n" result
Some result
...we can now do an all-or-nothing mapping of a list of integers with tryMap like this:
> let all = tryMap tryFac [1..5];;
The factorial of 1 is 1
The factorial of 2 is 2
The factorial of 3 is 6
The factorial of 4 is 24
The factorial of 5 is 120
val all : int list option = Some [1; 2; 6; 24; 120]
> let nothing = tryMap tryFac [1;2;-3;4;5];;
The factorial of 1 is 1
The factorial of 2 is 2
The factorial of -3 cannot be computed.
val nothing : int list option = None
Afterwards it would be easy to, for example, calculate the sum of these values - if they could all be computed, that is.
Now, my question is:
Is there a simpler/cleaner way to implement this tryMap function? (Besides not being so darn verbose, of course. :-P) I have a feeling something smart could be done using list expressions, maybe-expressions (from FSharpx) or maybe a combination of both, but I can't quite figure out how, at the moment. :-/
PS: If you have a better name than 'tryMap' for this function, don't hesitate to leave a comment. :-)
Update 1:
I have come up with this version, which is very close to what I had in mind, except it sadly doesn't short-circuit. :-/
let tryMap : ('T -> 'U option) -> 'T list -> 'U list option =
fun mapping list -> maybe { for value in list do return mapping value }
Note: This uses FSharpx' maybe-expressions.
Update 2:
Thanks to Tomas Petricek
, I got an idea for an alternative:
let tryMap (mapping : 'T -> 'U option) (list : 'T list) : 'U list option =
List.fold
(
fun (cont,quit) value ->
if quit then
(cont,quit)
else
match mapping value with
| None -> (cont,true)
| Some r -> ((fun rs -> (r::rs)) >> cont,quit)
)
(id,false)
list
|> (fun (cont,quit) -> if quit then None else Some (cont []))
This function stops mapping after it maps to its first None
value. When that happens, quit
will be true
, and the remaining elements won't be mapped. Afterwards, if quit
is true
, the partly mapped list is discarded and None
is returned. If it never maps to None
, it will have ended up building a continuation for constructing the mapped list.
It's still quite large though, and now it only does a "light" short circuit, in the sense that it stops trying to map the list, but it still traverses it, since that's how a fold works. :-/
Based on answers and comments by Daniel and Søren Debois, I've come up with the following:
let tryMap f xs =
let rec loop ys = function
| [] -> maybe { return List.rev ys }
| x::xs -> maybe { let! y = f x in return! loop (y::ys) xs }
loop [] xs
I'm not quite sure if it's tail-recursive or not though, since I'm not a total expert in the inner workings of computation- (or in this case specifically: maybe-) expressions.
What do you guys think of this? :-)