fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 21 forks source link

Add roundTo #1242

Open cmeeren opened 1 year ago

cmeeren commented 1 year ago

I propose we add roundTo: int -> 'a -> 'a that is similar to the existing round but rounds to the specified number of decimal places.

The existing way of approaching this problem in F# is to use System.Math.Round, which is not pipe-friendly. Defining a pipe-friendly helper for this that works with multiple number types is non-trivial.

Pros and Cons

The advantages of making this adjustment to F# are: Improved utility out of the box.

The disadvantages of making this adjustment to F# are: None?

Extra information

Estimated cost (XS, S, M, L, XL, XXL): S

Affidavit (please submit!)

Please tick this by placing a cross in the box:

Please tick all that apply:

For Readers

If you would like to see this issue implemented, please click the :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.

charlesroddie commented 1 year ago

I am not in favour:

So I believe the existing Math.Round etc API is good.

The only thing wrong with Math.Round is the implementation, throwing for negative decimal places. While creating an F# round might be a chance to have a correct implementation, it's better to fix at the source, in dotnet/runtime, rather than creating fixed versions in F#, if possible.

cmeeren commented 1 year ago

This issue isn't fully specified as not all types 'a can be rounded. The idea that this could be generic is unlikely to work.

It would work the same as the existing round, which uses SRTP and requires a Round member IIRC.

The value seems low as if any function is in non curried form the user can easily create a curried version, and vice versa. So creating duplicates of existing methods and exposing them purely in order to have non-curried or curried forms doesn't seem worth it.

It is not trivial to create a curried version in this case, since round (and therefore a generic roundTo) uses static member constraints. I haven't looked at the implementation of round, but if you do, I am sure you will agree that it is not something that any F# user can trivially write. (I am happy to be proven wrong, of course.)

Rounding is a complex operation and likely to be used in places which can easily go wrong and using a specialized function Math.Round (or equivalents for other numerics types), with MidpointRounding overloads is the most explicit.

Could you elaborate a bit more on this? I fail to see the big issue here, or how it is different to the existing round. I have either a float, a single, a decimal or similar that I want to round using normal rounding rules, just like round, except that I want to specify the number of decimal places.

BentTranberg commented 1 year ago

Just one additional idea on top of this. I am surprised that System.Math.Round can't round int with a negative digits argument. That is something that I actually need in many places in my source, and then also with several of the variations in the optional third argument - the MidpointRounding.

let x: int = 12345678 // millimeters, integral type
Math.Round(x, -1) |> Console.WriteLine // 12345680
Math.Round(x, -2) |> Console.WriteLine // 12345700
Math.Round(x, -2, MidpointRounding.ToZero) |> Console.WriteLine // 12345600

In my source, which deals heavily with lengths of wood, I have standardized internally exclusively on millimeters. In many places in the source these lengths have to be truncated or rounded, in various ways, to centimeters, decimeters and meters. I do that while still keeping the internal representation in millimeters. So I have this whole bunch of functions that do all the calculations to truncate and round to these various precisions for int16, int32 and int64. It would've been so much easier if Math.Round could have taken care of all of this for me.

So, obviously, the point is: If we get a roundTo, perhaps it can do this as well. I understand it might complicate the implementation for normal use, and also maybe throw coders off somehow (... I mean, a negative argument? ...), so that it's better to have another function, and perhaps even put it in a library such as FSharpPlus, if it isn't there already. But I would like it...