listf#mappingshort-circuitingpartial-functions

Short circuiting a list mapping with a partial function


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. :-/


Solution

  • 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? :-)