-
Notifications
You must be signed in to change notification settings - Fork 226
Description
Problem
When Float() is called with a Numeric argument, type checkers infer Float? instead of Float. This causes false positive NoMethod errors:
module NumericToFTest
def self.calculate(n)
Float(n) / 100.0
end
endmodule NumericToFTest
def self.calculate: (Numeric n) -> Float
endSteep error:
lib/numeric_to_f.rb:8:13: [error] Type `(::Float | nil)` does not have method `/`
│ Diagnostic ID: Ruby::NoMethod
Root Cause
The current Kernel#Float overloads are:
def self?.Float: (_ToF float_like, ?exception: true) -> Float
| (_ToF float_like, exception: bool) -> Float?
| (untyped, ?exception: bool) -> Float?The _ToF interface requires to_f: () -> Float. However, Numeric does not declare to_f in RBS (only its subclasses Integer, Float, Rational do). So when a parameter is typed as Numeric, it doesn't satisfy _ToF, and overload resolution falls through to (untyped, ...) -> Float?.
This matches Ruby's source: Numeric is abstract and doesn't define to_f—only its concrete subclasses do.
Ruby Implementation
The Float() function in object.c handles built-in Numeric types (Integer, Float, Rational) directly in C without calling to_f. For custom Numeric subclasses, it falls back to calling to_f.
Related PRs
- [Kernel] Tweak signatures for conversion methods, and add unit tests in #2683 "[Kernel] Tweak signatures for conversion methods" by @sampersand - directly working on Kernel conversion methods including
Float(). Notes thatComplex(Numeric) can never return nil. - Deprecate top-level float #2695 "Deprecate top-level float" by @sampersand - proposes using
Numeric & _ToFpattern (see Math'stype double = Numeric & _ToF).
This issue may be addressed by or complementary to #2683.
Proposed Solutions
EDIT: These proposed solutions were of poor quality.
Click to see poor quality proposals.
Option A: Add to_f to Numeric
Add a to_f declaration to Numeric in core/numeric.rbs. This makes Numeric satisfy _ToF, so the existing (_ToF, ...) -> Float overload matches.
class Numeric
include Comparable
+ # Converts self to a Float. Subclasses must implement this method.
+ def to_f: () -> Float
+
# -self -> selfPros:
- Simpler, smaller change
- Fixes the issue at the root
Cons:
Numericdoesn't actually defineto_fin Ruby—only subclasses do- Custom Numeric subclasses might not implement
to_f(thoughFloat()would fail at runtime anyway)
Option B: Add Numeric overload to Float()
Add explicit Numeric overloads to Kernel#Float in core/kernel.rbs.
def self?.Float: (_ToF float_like, ?exception: true) -> Float
+ | (Numeric numeric, ?exception: true) -> Float
| (_ToF float_like, exception: bool) -> Float?
+ | (Numeric numeric, exception: bool) -> Float?
| (untyped, ?exception: bool) -> Float?Pros:
- Precisely documents what
Float()accepts - Doesn't make claims about
Numerichavingto_f
Cons:
- More complex overload list
Option C: Use Numeric & _ToF (per #2695)
Replace _ToF with Numeric & _ToF in Float() overloads, consistent with the pattern in Math and the direction of #2695.
- def self?.Float: (_ToF float_like, ?exception: true) -> Float
- | (_ToF float_like, exception: bool) -> Float?
+ def self?.Float: (Numeric & _ToF float_like, ?exception: true) -> Float
+ | (Numeric & _ToF float_like, exception: bool) -> Float?
| (untyped, ?exception: bool) -> Float?Pros:
- Consistent with Deprecate top-level float #2695 direction
- Matches the
Math::doublepattern
Cons:
- More restrictive than current (excludes non-Numeric
_ToFimplementors)
Testing Notes
The RBS test framework checks whether a call matches any overload. Since (untyped, ...) -> Float? matches any argument, runtime tests can't verify these overloads. The value is purely for static type checkers.
This issue includes creative contributions from Claude (Anthropic).