Break the iteration and got the values and states?

Viewed 88

I need to call a function on each item in the list; and quit immediately if the function returns -1. I need to return the sum of results of the function and a string of "Done" or "Error".

let input = seq { 0..4 } // fake input

let calc1 x = // mimic failing after input 3. It's a very expensive function and should stop running after failing
    if x >= 3 then -1 else x * 2

let run input calc = 
    input 
    |> Seq.map(fun x -> 
        let v = calc x
        if v = -1 then .... // Error occurred, stop the execution if gets -1. Calc will not work anymore
        v)
     |> Seq.sum, if hasError then "Error" else "Done"  

run input calc // should return (6, "Error")
run input id   // should return (20, "Done")
3 Answers

The simplest way to effectively achieve exactly what is asked in idiomatic manner would be to use of an inner recursive function for traversing the sequence:

let run input calc =
    let rec inner unprocessed sum =
        match unprocessed with
        | [] -> (sum, "Done")
        | x::xs -> let res = calc x
                   if res < 0 then (sum, "Error") else inner xs (sum + res)
    inner (input |> Seq.toList) 0

Then run (seq {0..4}) (fun x -> if x >=3 then -1 else x * 2) returns (6,"Error") while run (seq [0;1;2;1;0;0;1;1;2;2]) (fun x -> if x >=3 then -1 else x * 2) returns (20, "Done")

More efficient version of the same thing shown below. This means it is now essentially a copy of @GeneBelitski's answer.

let run input calc = 
    let inputList = Seq.toList input
    let rec subrun inp acc = 
        match inp with
        | [] -> (acc, "Done")
        | (x :: xs) -> 
            let res = calc x
            match res with
            | Some(y) -> subrun xs (acc + y)
            | None -> (acc, "Error")
    subrun inputList 0

Note that this function below is EXTREMELY slow, likely because it uses Seq.tail (I had thought that would be the same as List.tail). I leave it in for posterity.

The easiest way I can think of for doing this in F# would be to use a tail-recursive function. Something like

let run input calc = 
    let rec subrun inp acc = 
        if Seq.isEmpty inp then
            (acc, "Done")
        else
            let res = calc (Seq.head inp)
            match res with
            | Some(x) -> subrun (Seq.tail inp) (acc + x)
            | None -> (acc, "Error")
    subrun input 0

I'm not 100% sure just how efficient that would be. In my experience, sometimes for some reason, my own tail-recursive functions seem to be considerably slower than using the built-in higher-order functions. This should at least get you to the right result.


The below, while apparently not answering the actual question, is left in just in case it is useful to someone.

The typical way to handle this would be to make your calc function return either an Option or Result type, e.g.

let calc1 x = if x = 3 then None else Some(x*2)

and then map that to your input. Afterwards, you can fairly easily do something like

|> Seq.exists Option.isNone

to make see if there are Nones in the resulting seq (you can pipe it to not if you want the opposite result).

If you just need to eliminate Nones from the list, you can use

Seq.choose id

which will eliminate all Nones while leaving the Options intact.

For summing the list, assuming that you have used choose to be left with just the Somes, then you can do

Seq.sumBy Option.get

Here is a monadic way of doing it using the Result monad.

First we create function calcR that if calc returns -1 returns Error otherwise returns Ok with the value:

let calcR f x = 
    let r = f x
    if  r = -1 then Error "result was = -1" else
    Ok  r

Then, we create function sumWhileOk that uses Seq.fold over the input, adding up the results as long as they are Ok.

let sumWhileOk fR = 
    Seq.fold(fun totalR v -> 
        totalR 
        |> Result.bind(fun total -> 
            fR v 
            |> Result.map      (fun r -> total + r) 
            |> Result.mapError (fun _ -> total    )
        ) 
    ) (Ok 0)

Result.bind and Result.map only invoke their lambda function if the supplied value is Ok if it is Error it gets bypassed. Result.mapError is used to replace the error message from calcR with the current total as an error.

It is called this way:

input |> sumWhileOk (calcR id)
// returns: Ok 10

input |> sumWhileOk (calcR calc1)
// return:  Error 6
Related