Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F# stop Seq.map when a predicate evaluates true

Tags:

map

f#

seq

I'm currently generating a sequence in a similar way to:

migrators
|> Seq.map (fun m -> m())

The migrator function is ultimately returning a discriminated union like:

type MigratorResult =
| Success of string * TimeSpan
| Error of string * Exception

I want to stop the map once I encounter my first Error but I need to include the Error in the final sequence.

I have something like the following to display a final message to the user

match results |> List.rev with
| [] -> "No results equals no migrators"
| head :: _ ->
   match head with
   | Success (dt, t) -> "All migrators succeeded"
   | Error (dt, ex) -> "Migration halted owing to error"

So I need:

  1. A way to stop the mapping when one of the map steps produces an Error
  2. A way to have that error be the final element added to the sequence

I appreciate there may be a different sequence method other than map that will do this, I'm new to F# and searching online hasn't yielded anything as yet!

like image 640
Clint Avatar asked Nov 12 '14 15:11

Clint


2 Answers

I guess there are multiple approaches here, but one way would be to use unfold:

migrators 
|> Seq.unfold (fun ms ->
    match ms with
    | m :: tl -> 
        match m () with
        | Success res -> Some (Success res, tl)
        | Error res   -> Some (Error res, [])
    | [] -> None)
|> List.ofSeq

Note the List.ofSeq at the end, that's just there for realizing the sequence. A different way to go would be to use sequence comprehensions, some might say it results in a clearer code.

like image 152
scrwtp Avatar answered Oct 21 '22 15:10

scrwtp


The ugly things Tomaš alludes to are 1) mutable state, and 2) manipulation of the underlying enumerator. A higher-order function which returns up to and including when the predicate holds would then look like this:

module Seq =
    let takeUntil pred (xs : _ seq) = seq{
        use en = xs.GetEnumerator()
        let flag = ref true
        while !flag && en.MoveNext() do
            flag := not <| pred en.Current
            yield en.Current }

seq{1..10} |> Seq.takeUntil (fun x -> x % 5 = 0)
|> Seq.toList
// val it : int list = [1; 2; 3; 4; 5]

For your specific application, you'd map the cases of the DU to a boolean.

(migrators : seq<MigratorResult>)
|> Seq.takeUntil (function Success _ -> false | Error _ -> true)
like image 42
kaefer Avatar answered Oct 21 '22 14:10

kaefer