Best way to do tryMax and tryMin in F#?
Asked Answered
S

2

6

Suppose I have a seq and I want to return the largest if there are any elements or None otherwise. F# does not appear to have this built-in.

Here is my attempt:

let tryMax xs = 
  if Seq.isEmpty xs
  then 
    None
  else 
    Seq.max xs |> Some

let tryMin xs = 
  if Seq.isEmpty xs
  then 
    None
  else 
    Seq.min xs |> Some
  • Are there any problems with this approach?
  • Is there a built-in solution for this?
Solitude answered 29/5, 2020 at 9:37 Comment(0)
P
2

FWIW, here's tryMinBy as well:

let tryMinBy projection (items : seq<_>) =
    use e = items.GetEnumerator()
    if e.MoveNext() then
        let mutable minItem = e.Current
        let mutable minValue = projection minItem
        while e.MoveNext() do
            let value = projection e.Current
            if value < minValue then
                minItem <- e.Current
                minValue <- value
        Some minItem
    else
        None

The full suite:

module Seq

let tryMinBy projection (items : seq<_>) =
  use e = items.GetEnumerator ()

  if e.MoveNext ()
  then
    let mutable minItem = e.Current
    let mutable minValue = projection minItem

    while e.MoveNext () do
      let value = projection e.Current

      if value < minValue
      then
        minItem <- e.Current
        minValue <- value

    Some minItem
  else
    None

let tryMaxBy projection (items : seq<_>) =
  use e = items.GetEnumerator ()

  if e.MoveNext ()
  then
    let mutable maxItem = e.Current
    let mutable maxValue = projection maxItem

    while e.MoveNext () do
      let value = projection e.Current

      if value > maxValue
      then
        maxItem <- e.Current
        maxValue <- value

    Some maxItem
  else
    None

let tryMin (items : seq<_>) =
  use e = items.GetEnumerator ()

  if e.MoveNext ()
  then
    let mutable minItem = e.Current

    while e.MoveNext () do
      if e.Current < minItem
      then
        minItem <- e.Current

    Some minItem
  else
    None

let tryMax (items : seq<_>) =
  use e = items.GetEnumerator ()

  if e.MoveNext ()
  then
    let mutable maxItem = e.Current

    while e.MoveNext () do
      if e.Current > maxItem
      then
        maxItem <- e.Current

    Some maxItem
  else
    None
Patronymic answered 2/9, 2020 at 19:45 Comment(1)
I appreciate expanding on my original answer below, where you may find the explanation & usage of this approach and an alternative. Though I'd recommend using min and max build-in functions as they may be better inlined/optimized and add readability.Mislay
M
7

I think your approach is generally good. There was an answer that is now deleted that suggested to use try/with to prevent double-evaluation of the first item by catching the error for empty sequences, but that too can be expensive.

If you want to prevent double evaluation, you can use Seq.cache, or not use Seq at all (use List or Array instead). Or use fold, which iterates only once:

module Seq =
    let tryMin sq =
        sq
        |> Seq.fold(fun x y -> 
            match x with None -> Some y | Some x -> Some(min x y)) None

Usage:

> Seq.tryMin Seq.empty<int>;;
val it : int option = None

> Seq.tryMin (Seq.singleton 2L);;
val it : int64 option = Some 2L

> Seq.tryMin (seq { 2; 3});;
val it : int option = Some 2

> Seq.tryMin (seq { 2; -3});;
val it : int option = Some -3

A potentially faster method (I didn't time it), is to prevent the creation of option on each min- or max-calculation result, and at the same time preventing multiple iterations of the first item.

This should have much less GC pressure too ;).

module Seq =
    let tryMin (sq: seq<_>) =
        use e = sq.GetEnumerator()

        // this returns false if there is no first item
        if e.MoveNext() then
            let mutable result = e.Current
            while e.MoveNext() do
                result <- min e.Current result

            Some result
        else
            None

Usage:

> Seq.tryMin Seq.empty<int>;;
val it : int option = None

> Seq.tryMin (Seq.singleton 2L);;
val it : int64 option = Some 2L

> Seq.tryMin (seq { 2; 3});;
val it : int option = Some 2

> Seq.tryMin (seq { 2; -3});;
val it : int option = Some -3
Mislay answered 29/5, 2020 at 12:10 Comment(2)
It's very annoying that these aren't built in. I wonder if there's a proposal to add them. All four.Scorecard
@BentTranberg, I agree. It's only mentioned here: github.com/fsharp/fslang-suggestions/issues/…, but not as a suggestion to add it. Feel free to create a request in the F# suggestions repo here: github.com/fsharp/fslang-suggestions.Mislay
P
2

FWIW, here's tryMinBy as well:

let tryMinBy projection (items : seq<_>) =
    use e = items.GetEnumerator()
    if e.MoveNext() then
        let mutable minItem = e.Current
        let mutable minValue = projection minItem
        while e.MoveNext() do
            let value = projection e.Current
            if value < minValue then
                minItem <- e.Current
                minValue <- value
        Some minItem
    else
        None

The full suite:

module Seq

let tryMinBy projection (items : seq<_>) =
  use e = items.GetEnumerator ()

  if e.MoveNext ()
  then
    let mutable minItem = e.Current
    let mutable minValue = projection minItem

    while e.MoveNext () do
      let value = projection e.Current

      if value < minValue
      then
        minItem <- e.Current
        minValue <- value

    Some minItem
  else
    None

let tryMaxBy projection (items : seq<_>) =
  use e = items.GetEnumerator ()

  if e.MoveNext ()
  then
    let mutable maxItem = e.Current
    let mutable maxValue = projection maxItem

    while e.MoveNext () do
      let value = projection e.Current

      if value > maxValue
      then
        maxItem <- e.Current
        maxValue <- value

    Some maxItem
  else
    None

let tryMin (items : seq<_>) =
  use e = items.GetEnumerator ()

  if e.MoveNext ()
  then
    let mutable minItem = e.Current

    while e.MoveNext () do
      if e.Current < minItem
      then
        minItem <- e.Current

    Some minItem
  else
    None

let tryMax (items : seq<_>) =
  use e = items.GetEnumerator ()

  if e.MoveNext ()
  then
    let mutable maxItem = e.Current

    while e.MoveNext () do
      if e.Current > maxItem
      then
        maxItem <- e.Current

    Some maxItem
  else
    None
Patronymic answered 2/9, 2020 at 19:45 Comment(1)
I appreciate expanding on my original answer below, where you may find the explanation & usage of this approach and an alternative. Though I'd recommend using min and max build-in functions as they may be better inlined/optimized and add readability.Mislay

© 2022 - 2024 — McMap. All rights reserved.