Join GitHub today
GitHub is home to over 40 million developers working together to host and review code, manage projects, and build software together.Sign up
Add 'try' versions of Set.minElement and Set.maxElement #803
Title of Suggestion
I propose we add 'try' version of minElement and maxElement (and corresponding class methods) in Microsoft.FSharp.Collections.Set module.
The existing way of approaching this problem in F# is to first check if the set is empty. Alternatively wrap the current functions in a try .. with since they may throw an ArgumentException.
Pros and Cons
The advantages of making this adjustment to F# are readily available type safe options for retrieving set elements without exceptions. This is already idiomatic in other collection modules.
The disadvantages of making this adjustment to F# are enlarging the Set module and class.
Estimated cost (XS, S, M, L, XL, XXL): XS
Affidavit (please submit!)
Please tick this by placing a cross in the box:
Please tick all that apply:
@abelbraaksma I'm not a fan of having to throw and catch an exception (even implicitly) to handle empty collections :/
@gusty Interesting idea. It's longer than an explicit
set  |> fun s -> if Set.isEmpty s then None else Some (Set.maxElement s)
Another possibility that would provide this functionality without adding too many functions: adding a function
module Set = val ifEmpty : ifEmpty:'u -> ifNotEmpty:(Set<'t> -> 'u) -> collection:Set<'t> -> 'u // Usage: set  |> Set.ifEmpty None (Set.maxElement >> Some)
This also allows using something else than Option.
set  |> Set.ifEmpty 0 Set.maxElement set  |> Set.ifEmpty ValueNone (Set.maxElement >> ValueSome)
@Tarmil, I'm not suggesting try/catching. That is slow if the common case can be that a statement throws and exception, and it clutters code. I try (pun intended) to stay away from it where possible.
I meant something like this:
module Set = let tryNonEmpty s = if Set.isEmpty s then None else Some s module List = let tryNonEmpty l = if List.isEmpty l then None else Some l ....
set  |> Set.tryNonEmpty |> Option.map Set.maxElement  |> List.tryNonEmpty |> Option.map (List.reduce (+))
I think it ends up rather concise and relatively readable.
@gusty, I actually have
set  |> Option.whenFalse Set.isEmpty |> Option.map Set.maxElement  |> Option.whenFalse List.isEmpty |> Option.map (List.reduce (+))
side thought: I have these functions in my default Option extension project for a while, but looking through my code, I didn't end up using them very often. As @dsyme often mentions, oftentimes, explicit branching and explicit
@Tarmil, this is somewhat in the same league as the recently added
So perhaps we'd need both approaches ;).
As another aside: it may be wiser to simply skip to a
The point of my original proposal is that it is flexible enough to do both and more.
// Setting the default value: myList |> List.ifEmpty  id // (an additional With variant would avoid allocating the list unless needed) // Turning into an option: myList |> List.ifEmpty None Some // Call an aggregation or return a default value: myList |> List.ifEmpty 0 List.min // Call an aggregation and return an option: myList |> List.ifEmpty None (List.min >> Some)
Actually, I think
Also, they would force the same type, while your
I think that both with my proposal and yours, they are not very flexible. @gusty's proposal, or
For instance, I have many requirements for "more-than-one" situations in my own field, which would easily be covered with the predicate-based functions:
// not necessarily the best way, but just to illustrate: // this will run doSomeCalculations only when size of list is 2+, // without having to iterate the list manually myList |> List.withPredicate (List.tryTail >> Option.bind List.tryTail >> Option.isSome) doSomeCalculations
I understand your reasoning but I disagree. If we go that way, for consistency, we should add the
List.average List.averageBy List.chunkBySize List.exactlyOne List.fold2 List.foldBack2 List.forall2 List.iter2 List.iteri2 List.map2 List.map3 List.mapi2 List.min List.minBy List.max List.maxBy List.permute List.reduceBack List.skip List.split List.splitInto List.take List.transpose List.windowed List.zip List.zip3
To be fair, I'm a little surprised this list is so long, and I also noted that some on that list do not explicitly stated that they can raise an exception.
But my point is simple: allow for some function (name TBD) that allows easy composition with the existing functions for either a common predicate (empty, equal lengths) or a generalized predicate.
Yes, but that's mainly due to the verbose syntax of having to write repeatedly the name of the module
@abelbraaksma The problem with using a combinator to make a function safe is you still have to check carefully that you've combined the functions safely. I would prefer to have all of the
I would go so far as to say I'd prefer to have the unsafe versions moved to an
@theprash, I agree, but simple defensive programming means you use
Just using (for example)
Totally safe code is still a task for the programmer to look out for. If all statements are safe, we'd be out of a job soon ;). Though it may be interesting to write a safe library of all core functions and use that as an alternative. Something like SafeF# ;).
@abelbraaksma We already have
Then we could optionally lint for the usage of the unsafe functions, giving people the chance to write code that forces them to consider all cases and use
The more cognitive load we can automate away from the programmer without significant cost the better. They will still have plenty of other things to think about.
@theprash I know, I was responding to your comment that we shouldn't have unsafe functions to begin with, it put them in
About the downside: only exponential growth of functions. It's already hard to weed through the list of your unsure what you need. But I'm not against the idea. Just that when I was proposing similar ideas (see
Indeed, though currently, F#Lint doesn't work out of the box in the VS IDE. This would certainly help new users, but now they're probably not using it, I'd argue it'd be a fine addition and better user experience if it were added to the standard F# installation by default.
Overall I think we'd be much helped if we can create a list that's guaranteed 1+. It should be a list, so that you can use the same functions. Then Lint can understand the type and doesn't have to warn when using