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
end
module NumericToFTest
def self.calculate: (Numeric n) -> Float
end
Steep 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
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 -> self
Pros:
- Simpler, smaller change
- Fixes the issue at the root
Cons:
Numeric doesn't actually define to_f in Ruby—only subclasses do
- Custom Numeric subclasses might not implement
to_f (though Float() 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
Numeric having to_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:
Cons:
- More restrictive than current (excludes non-Numeric
_ToF implementors)
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).
Problem
When
Float()is called with aNumericargument, type checkers inferFloat?instead ofFloat. This causes false positiveNoMethoderrors:Steep error:
Root Cause
The current
Kernel#Floatoverloads are:The
_ToFinterface requiresto_f: () -> Float. However,Numericdoes not declareto_fin RBS (only its subclasses Integer, Float, Rational do). So when a parameter is typed asNumeric, it doesn't satisfy_ToF, and overload resolution falls through to(untyped, ...) -> Float?.This matches Ruby's source:
Numericis abstract and doesn't defineto_f—only its concrete subclasses do.Ruby Implementation
The
Float()function inobject.chandles built-in Numeric types (Integer, Float, Rational) directly in C without callingto_f. For custom Numeric subclasses, it falls back to callingto_f.Related PRs
Float(). Notes thatComplex(Numeric) can never return nil.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_fto NumericAdd a
to_fdeclaration toNumericincore/numeric.rbs. This makesNumericsatisfy_ToF, so the existing(_ToF, ...) -> Floatoverload matches.Pros:
Cons:
Numericdoesn't actually defineto_fin Ruby—only subclasses doto_f(thoughFloat()would fail at runtime anyway)Option B: Add Numeric overload to Float()
Add explicit
Numericoverloads toKernel#Floatincore/kernel.rbs.Pros:
Float()acceptsNumerichavingto_fCons:
Option C: Use
Numeric & _ToF(per #2695)Replace
_ToFwithNumeric & _ToFinFloat()overloads, consistent with the pattern in Math and the direction of #2695.Pros:
Math::doublepatternCons:
_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).