Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add :float support #1055

Open
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

seanstrom
Copy link

Summary

Notes

  • I've tried to add float support to Malli while being mindful of the potentially different meanings of float between Clojure and ClojureScript. Specifically, I've tried to make Malli to treat floats as Java/Clojure floats (instances of Float) while using clj, and fallback to doubles while using cljs

Copy link
Author

@seanstrom seanstrom left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Self-review comments 📖

Comment on lines 457 to 470
(defmethod -schema-generator :float [schema options]
(let [max-float #?(:clj Float/MAX_VALUE :cljs (.-MAX_VALUE js/Number))
min-float #?(:clj Float/MIN_VALUE :cljs (.-MIN_VALUE js/Number))
ceil-max (fn [v] (if (nil? v) max-float (min max-float v)))
floor-min (fn [v] (if (nil? v) min-float (max min-float v)))
props (m/properties schema options)
min-max-props (-min-max schema options)
infinite? (get props :gen/infinite? false)
NaN? (get props :gen/NaN? false)]
(gen/double* (merge {:infinite? infinite?
:NaN? NaN?}
(when (or (not infinite?) NaN?)
{:min (floor-min (:min min-max-props))
:max (ceil-max (:max min-max-props))})))))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I was unsure how to exactly generate a float or a instance of Float, but I attempted to constrain the generated values to something that could be coerced into a Float instance.

If someone knows how/where to coerce the generated value into a Float instance then please let me know 🙏

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call (gen/fmap float (gen/double* ...)).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although that doesn't work for ##Inf and ##-Inf. Might not make sense to allow them in a float generator, at least on the jvm.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Call (gen/fmap float (gen/double* ...)).

Ah perfect thank you for this 🙏

Although that doesn't work for ##Inf and ##-Inf. Might not make sense to allow them in a float generator, at least on the jvm.

Yeah that makes sense, I'll try setting infinite? to always be false.

Comment on lines +70 to +85
(defn parse-float [s]
#?(:clj
(if (string? s)
(try
(Float/parseFloat s)
(catch NumberFormatException _ nil))
(throw (IllegalArgumentException.
(str "Expected string, got " (if (nil? s) "nil" (-> s class .getName))))))
:cljs
(parse-double s)))

(defn -string->float [x]
(if (string? x)
(or (parse-float x) x)
x))

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here I'm providing a custom definition for parsing floats from strings. I could not find a function that used Float/parseFloat in this way, so I copied and modified a similar function from Clojure source code.

Comment on lines 64 to 82

(testing "float properties"
(let [infinity? #(or (= % ##Inf)
(= % ##-Inf))
NaN? (fn [x]
(#?(:clj (fn [v]
(or (infinity? v) (Float/isNaN v)))
:cljs js/isNaN)
x))
special? #(or (NaN? %)
(infinity? %))
test-presence (fn [f options]
(some f (mg/sample [:float options]
{:size 1000})))]
(is (test-presence infinity? {:gen/infinite? true}))
(is (test-presence NaN? {:gen/NaN? true}))
(is (test-presence special? {:gen/infinite? true
:gen/NaN? true}))
(is (not (test-presence special? nil)))))
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was a bit tricky writing this test since I want to use Float/isNaN, but sometimes when generating Infinity the tests would error because of the value being too big to coerce into a float.

Note, I don't think this test generates Float instances, so I think it should be updated to check for that but I'm unsure how to do that. If someone know how please let me know 🙏

@@ -454,6 +454,20 @@
{:infinite? (get props :gen/infinite? false)
:NaN? (get props :gen/NaN? false)})
(-min-max schema options))))
(defmethod -schema-generator :float [schema options]
(let [max-float #?(:clj Float/MAX_VALUE :cljs (.-MAX_VALUE js/Number))
min-float #?(:clj Float/MIN_VALUE :cljs (.-MIN_VALUE js/Number))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think Float/MIN_VALUE is the minimum float, I think it's (- Float/MAX_VALUE).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yes good catch 🙏

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just meant for JVM, (.-MIN_VALUE js/Number) seems appropriate for cljs.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want the min-float to be a negative number? Because I think (.-MIN_VALUE js/Number) would be the smallest positive number.

@seanstrom seanstrom requested a review from frenchy64 May 16, 2024 14:16
@frenchy64
Copy link
Contributor

frenchy64 commented May 17, 2024 via email

@seanstrom
Copy link
Author

Do we even need to specify the bounds on cljs since it looks like the same as double?

Yeah good point, we probably don't need to do it, so I've changed it to only set the bounds for the clj and allow cljs to behave as doubles. I've also made sure to allow for infinity to be generated for cljs too.

min-max-props (-min-max schema options)
min-max-clamped #?(:clj {:min (floor-min (:min min-max-props))
:max (ceil-max (:max min-max-props))}
:cljs min-max-props)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a compelling reason to clamp the bounds, perhaps an error should be thrown instead.

Relatedly, the bounds should be coerced to float so then floating point numbers are generated regardless of bounds like in this PR #1052

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't think of a compelling reason to clamp the bounds, perhaps an error should be thrown instead.

Well the main reason I thought to clamp the bounds is because we're using gen/double*, which can produce numbers that will lead to thrown exceptions when trying to coerce them into floats. So I think that means we always need to clamp the bounds, but maybe we can also throw an error if someone tries to override those boundaries (?).

the bounds should be coerced to float so then floating point numbers are generated regardless of bounds

I'll push up a commit to coerce the bounds to floats too 👍

@seanstrom seanstrom requested a review from frenchy64 May 23, 2024 09:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants