Skip to content

Float(Numeric) returns Float?, should return Float #2793

@Kerrick

Description

@Kerrick

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions