In Ruby 4.0, what class does (1..30).reduce(:*) return, and what happened to Fixnum and Bignum?
answer
- one class, no overflow
- unified in Ruby 2.4
- constants removed in 3.2: NameError
- small values are immediates
- to_f keeps only 53 bits
basics
~10 sIt 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 linesbig = (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"
endgo deeper
Remember that Ruby has one Integer class with no overflow, and that Fixnum and Bignum no longer exist in current Ruby.
Explain the hidden split between immediates and heap integers, and show where precision is lost: converting to Float above 2 to the 53rd.
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.
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.