Conversation
Build was not working in 2025 on old Paket due to .NET 9 / 10 existing
float32 and UoM assertion functions
float32 and UoM assertion functions|
A few thoughts here. 32-bit variant namingI'm tracking with the float32 methods. I had to build some context before I understood the naming scheme though. How do you feel about a naming scheme like Would it be more clear to split the accuracy constants into two modules? So instead of having Units of measureIf I understand right, the motivation here is to operate on values that have units without the caller needing to unwrap those units? It feels like it adds some ceremony to the general case that I'm uncomfortable with. Maybe I'm not understanding fully. I'd like to hear more of what you had in mind. |
It matches precedent set elsewhere. For example: the BCL
It's certainly obvious what it is, but so wordy IMO. Which might disincentivize one too strongly from using
I could see that. For some reason I don't really like it stylistically, but that's super subjective and not a very good actual argument, haha. So I could live with that :)
Agreed.
I'd have no issue with that; makes perfect sense. Though if it were up to me I'd probably name it There's actually another option we haven't discussed: using SRTP to overload all the existing functions to work with both Pros:
Cons:
Though the first downside is somewhat mitigated by the fact that this wouldn't be a super complex use of SRTP's (we're not using it simulating monads or anything like FSharpPlus does for example). Just simple function overloading for
Yes it's to make it easier to assert against values with units-of-measure. It doesn't add any burden to the user of the library. Just the function definitions themselves. I basically just made it all generic with respect to UoM, as F# by default infers unitless (non-generic) by default. If you have a codebase that uses a lot of UoM, it gets pretty annoying to write tests for it. For example, given this code: type Point<[<Measure>] 'u> = { X: float<'u>; Y: float<'u> }
module Point =
let length (p: Point<'u>) : float<'u> = sqrt ((p.X * p.X) + (p.Y * p.Y))
let create (x, y) = { X = x; Y = y }We have to write open Expecto.Expect
open Expecto.Flip
open FSharp.Data.UnitSystems.SI.UnitSymbols
[<Tests>]
let tests =
testCase "length" (fun () ->
(Point.length (Point.create (3.0<m>, 4.0<m>)))
|> float
|> Expect.floatClose "result" Accuracy.high 5.0<m>
)When we'd really just like to write: open Expecto.Expect
open Expecto.Flip
open FSharp.Data.UnitSystems.SI.UnitSymbols
[<Tests>]
let tests =
testCase "length" (fun () ->
// no cast needed
(Point.length (Point.create (3.0<m>, 4.0<m>)))
|> Expect.floatClose "result" Accuracy.high 5.0<m>
)Granted, this is a particularly simple case on its own, but when your code and tests start to get a little more complex, it gets more and more annoying. Also - this is an issue with all test frameworks anyway; it's just that Expecto here has a chance to actually do something about it since it is F#-aware and set itself apart in this area. And to reiterate - it's strictly backwards compatible. No changes needed for users who doesn't employ units of measure. |
|
One more thing: I just realized another approach to // NOTE: I've written what it would look like with UoM enabled -- this could still be done without that bit (just drop all the units)
type Accuracy<[<Measure>] 'u> =
{ absolute: float; relative: float<'u>
absolutef: float32; relative: float32<'u> }
module Accuracy =
let low<[<Measure>] 'u> = // ...
let high<[<Measure>] 'u> = // ...
// etcThen you just use the same single |
NamingSeems like we should get some more opinions. @ratsclub @haf @rynoV @Numpsy I.e. Statically Resolved Type ParametersI often wish for overloads in this kind of situation. But, I feel like overloads are non-standard in F#. Units of MeasureI guess I didn't look close enough. I'm onboard now. Doubled-up accuracy definitionsI prefer separate accuracy types. Grouping them in one type might consolidate our standard accuracy levels, but it creates incidental coupling. It assumes that users will only ever define accuracy levels that apply to both float and float32. |
|
I'm not sure about the 'f' vs '32' naming - i see the point about f being used for literals and some other math things, but then there are also a bunch of functions that use an f suffix to infer they do formatting ( |
|
Imo: |
The F# team discourages their In fact one advantage of doing so is that we could, for example, define a super generic I digress. RE: float naming: I just realized I had a brain fart in my last comments.
Well... C#'s Sounds like everything else is all good then? |
OverloadsIf you'd like to experiment with overloads, I think the next step would be to implement it on a function or two in a separate branch so we can have a concrete discussion. I'd be happy to collaborate if that's the direction you want to go. NamingI'm good with I think the only unresolved action items are naming and potentially splitting the Accuracy constants into two modules. Everything else was good. |
f13d874 to
52d980a
Compare
|
Great - just pushed some changes to split However in doing this I noticed there's still I also noticed there's no Overload experiment branch is here: https://github.com/jwosty/expecto/tree/float-overloads-srtp |
NamingHmm. It was my assumption that the nan and inifinity methods would follow a naming pattern similar to the float32Close and Accuracy32. Something like However, you make a good point that the F# constants are OverloadsI haven't given this a proper look yet. Here's the diff: jwosty/expecto@float-overloads...jwosty:expecto:float-overloads-srtp |
|
Some thoughts on overloads. First, thanks for taking the time to put together an example! I like the GenericMath module. It both separates out the math operations and confines as most of the SRTPs, making it easier to read around the SRTP syntax. Pros
Cons
Some notes so we don't forget if we move forward with overloads
I'm still not certain here. I'd say function signature clarity is pushing me toward separate functions (what we already have) instead of the overloads. |
|
@farlee2121 good analysis, pretty much agree with everything you've said there.
Agreed. The main way to mitigate this is through good documentation with concrete examples -- both external, and inline XML docs. And honestly this is one of the simpler applications of SRTP; we could get really simple and just say "where 'a is float or float32". (side note - now that I think about it, there's no reason these operators couldn't also just have |
|
I'm a bit unsure on how to understand your comment. Are you still wanting to move forward with the overload approach? |
|
@farlee2121 If it were my library, I'd probably do the operator overloads. But it is not my library so it's ultimately up to you and can understand wanting to not do the overloads if that's the choice you make |
|
Hmm. I want to shy away from seeing it as my library. Expecto has only been possible as a community collaboration. Also due to the collaborative nature, I'm increasingly leaning toward the more self-documenting separate methods instead of overloads. Keeping the contribution effort and documentation burden as low as we can seems prudent. However, I can certainly be persuaded if anyone wants to speak up for overloads. |
|
Sure thing. Well, since you're asking :) - my vote would be for the overloads, as reducing noisiness and increasing ergonomics for test code is a big deal IMO. Same reason why other test frameworks Assert classes often heavily rely on overloading. I personally think that, as long as we pay special attention to docs around this part, it outweighs the downside in slightly increased barrier to entry. |
|
@farlee2121 thoughts? I'd love to get some form of this merged |
|
Thanks for the nudge. I was waiting for a tie breaker and then it slipped my attention. I seem to remember that this PR was in a satisfactory state, but the debate was if we should instead proceed from the branch demonstrating overloads. Since this PR is already satisfactory, would you be ok if I merge it? |
I've added float32 versions of all the float assertion functions. I've also extended both the float and float32 versions of all of those to support floats/float32s with units of measure.
List of functions added:
floatCloseffloatLessThanOrCloseffloatGreaterThanOrClosefisNotNaNfisNotPositiveInfinityfisNotNegativeInfinityfisNotInfinityfFor example we can now write:
and: