skip to content

In Ruby 4.0, what class does (1..30).reduce(:*) return, and what happened to Fixnum and Bignum?

level: juniorimportance: should knowfreq 45%

answer

  1. one class, no overflow
  2. unified in Ruby 2.4
  3. constants removed in 3.2: NameError
  4. small values are immediates
  5. to_f keeps only 53 bits

basics

~10 s

It returns Integer. Ruby has one arbitrary-precision Integer class that grows instead of overflowing; Fixnum and Bignum were unified into Integer in Ruby 2.4 and their deprecated constants were removed in 3.2.

solid answer

~30 s

`(1..30).reduce(:*)` is `265252859812191058636308480000000`, and its class is `Integer`. Ruby integers do not overflow: small values live as immediates inside the reference itself, and larger ones switch transparently to a heap-allocated multi-word representation, but both report the same class. Ruby 2.4 unified `Fixnum` and `Bignum` into `Integer` and kept the two names only as deprecated aliases; Ruby 3.2 removed them, so `Fixnum` now raises `NameError`. The costs sit elsewhere: very large integers allocate and get slower as they grow, and converting one to `Float` rounds it to a 53-bit mantissa, so `9007199254740993.to_f.to_i` returns `9007199254740992`.

code

ruby · 12 lines
ruby
big = (1..30).reduce(:*)
big.class                    # => Integer
big.bit_length               # => 108

9007199254740993.to_f.to_i   # => 9007199254740992
100000000000000000000 + 0.5  # => 1.0e+20

begin
  42.is_a?(Fixnum)
rescue NameError => e
  e.message                  # => "uninitialized constant Fixnum"
end

go deeper

for a junior

Remember that Ruby has one Integer class with no overflow, and that Fixnum and Bignum no longer exist in current Ruby.

for a middle

Explain the hidden split between immediates and heap integers, and show where precision is lost: converting to Float above 2 to the 53rd.

for a senior

Point to the production consequences: allocation cost of huge integers in hot paths, old gems still naming Fixnum, and 64-bit IDs that survive in Ruby but not in double-based consumers.

for a principal

Frame the trade-off as correctness by default at the price of hidden cost: unlimited integers remove overflow bugs but move the risk to Float boundaries and performance.

## One Integer class, no overflow In Ruby every whole number is an instance of **`Integer`**, a subclass of `Numeric` that includes `Comparable`. There is no fixed width: an `Integer` holds as many digits as memory allows, and arithmetic that would overflow a 64-bit register in other runtimes simply produces a larger `Integer`. ```ruby big = (1..30).reduce(:*) big # => 265252859812191058636308480000000 big.class # => Integer big.bit_length # => 108 ``` The factorial of 30 needs 108 bits, well past a machine word, yet nothing in the code changes: no special constructor, no library, no `require`. ## What happened to Fixnum and Bignum Older Ruby exposed the two internal representations as two classes. That split is gone: | Ruby version | What `Fixnum` / `Bignum` mean | |---|---| | before 2.4 | two real classes under `Integer`; `1.class` was `Fixnum` | | 2.4 to 3.1 | unified into `Integer`; the names were deprecated aliases for `Integer` | | 3.2 and later (so 4.0) | constants removed; referencing `Fixnum` raises `NameError` | So in Ruby 4.0, `42.is_a?(Fixnum)` does not return `true` or `false` - it fails with `NameError: uninitialized constant Fixnum`. Code copied from old blog posts or old gems has to name `Integer` instead. ## Immediates and heap integers Unifying the class did not unify the storage. CRuby still keeps two representations internally: - **Small integers** are **immediates**: the value is encoded in the reference itself, so there is only ever one `1`, and no allocation happens when you compute it. - **Large integers** are heap objects holding an array of machine words, allocated and garbage-collected like any other object. - The switch between them is automatic in both directions and invisible at the language level: same class, same methods, same `==`. - `Integer#size` reports the bytes of the machine representation (`1.size` is `8` on a 64-bit build), and `Integer#bit_length` reports how many bits the value needs. The practical consequence is performance, not correctness: arithmetic on huge integers allocates and gets slower as the digit count grows. ## Where the precision ends: Float The unlimited precision belongs to `Integer` alone. The moment a value becomes a `Float` it is squeezed into an IEEE 754 double with a **53-bit mantissa**: 1. `9007199254740993` (2 to the 53rd, plus one) is an exact `Integer`. 2. `9007199254740993.to_f` must round to a neighbouring double; the tie goes to the even mantissa, `9007199254740992.0`. 3. `.to_i` then returns `9007199254740992` - one lost. Mixing types has the same effect: `100000000000000000000 + 0.5` returns the `Float` `1.0e+20`, and the half is gone. Keep integers as `Integer` (or move to `Rational` or `BigDecimal`) when every digit matters. ## Integer methods that rely on unlimited precision Because an `Integer` can hold any size, several core methods are designed to work on huge values without a library: | Method | Example | Result | |---|---|---| | `Integer.sqrt` | `Integer.sqrt(10)` | `3` - an exact integer square root, no Float detour | | `Integer#pow` with a modulus | `3.pow(100, 7)` | `4` - modular exponentiation without building the huge power | | `Integer#digits` | `1234.digits` | `[4, 3, 2, 1]` - least significant digit first | | `Integer#gcd`, `Integer#lcm` | `12.gcd(18)` | `6` | | `Integer#bit_length` | `255.bit_length` | `8` | `Integer.sqrt` matters exactly because of the Float boundary above: `Math.sqrt` converts its argument to a Float first, so for very large integers it returns an approximation, while `Integer.sqrt` stays exact. `pow` with a second argument is the idiomatic way to do modular arithmetic on large numbers, since it never materialises the full power. These methods also show why the old two-class model was a burden: every one of them had to work identically on `Fixnum` and `Bignum`, and user code that branched on the class was checking an implementation detail. Unifying the class removed that branch point while keeping the fast path for small values. ## What interviewers want to hear - Ruby integers **never overflow**; there is no maximum `Integer` constant to check against. - There is **one class**, `Integer`; `Fixnum` and `Bignum` are history, removed in 3.2. - The internal split still exists for **speed**: small values are immediates, large ones are heap objects. - Precision is lost only when a value crosses into **`Float`**, above 2 to the 53rd. - For identifiers such as 64-bit IDs from another system, this is why Ruby can hold them exactly while a double-based JSON consumer on the other side may not.

  • In Ruby, what does 100000000000000000000 + 0.5 return, and why?
    It returns the `Float` `1.0e+20`. Adding an `Integer` and a `Float` produces a `Float`, and a double has only a 53-bit mantissa, so at that magnitude the spacing between adjacent Floats is far larger than 0.5 and the half disappears. Keep the value as `Integer`, or use `Rational` or `BigDecimal`, when every digit matters.
  • In Ruby 4.0, how do you fix an old gem that checks x.is_a?(Fixnum) or x.is_a?(Bignum)?
    Replace both with `Integer`. The constants were deprecated aliases for `Integer` from 2.4 to 3.1 and were removed in 3.2, so on Ruby 4.0 the old check raises `NameError` before it even runs. If the code really meant "fits in 64 bits", compare the value against explicit bounds instead of relying on a class name.

saying these in an interview costs you the question

  • Ruby integers overflow or wrap around at 64 bits like a C long.
  • Fixnum and Bignum are still the classes of small and large integers in Ruby 4.0.
  • Converting any Integer to Float is lossless because Ruby numbers have unlimited precision.
  • Integers past 64 bits need require "bigdecimal" or a special constructor.