In Ruby 4.0, what does Struct.new return, and how do positional, keyword and keyword_init: construction behave?
answer
- returns a class, not an instance
- reader and writer per member
- omitted members read nil
- extra args: struct size differs
- keyword_init: true bans positional
basics
~10 sStruct.new returns a new class with a reader and a writer per member. Its instances take positional or, since Ruby 3.2, keyword arguments; omitted members are nil, and keyword_init: true makes construction keyword-only.
solid answer
~30 s`Fix = Struct.new(:lat, :lng)` returns a new subclass of `Struct` with `lat`, `lat=`, `lng` and `lng=`, plus `members`, `to_h`, `to_a`, `[]` and `Enumerable`. `Fix.new(52.52, 13.405)` fills members in order; trailing members you leave out are `nil`, and too many arguments raise `ArgumentError` ("struct size differs"). Since Ruby 3.2 the same class also accepts `Fix.new(lat: 52.52, lng: 13.405)` with no option, and an unknown key raises `ArgumentError`. `keyword_init: true` makes it keyword-only, so positional calls raise; `keyword_init: false` makes it positional-only, so a keyword call silently puts the whole Hash into the first member. Methods go in the block passed to `Struct.new`.
code
ruby · 14 linesGpsFix = Struct.new(:lat, :lng, :recorded_at)
fix = GpsFix.new(52.52, 13.405)
fix.recorded_at # => nil
fix.lat = 52.53 # writers exist: Struct is mutable
GpsFix.new(lat: 1, lng: 2) # keywords work without keyword_init (3.2+)
GpsFix.new(1, 2, 3, 4) # ArgumentError: struct size differs
GpsFix.new(lat: 1, alt: 9) # ArgumentError: unknown keywords: alt
Strict = Struct.new(:lat, :lng, keyword_init: true)
Strict.new(1, 2) # ArgumentError (wrong number of arguments)
Loose = Struct.new(:lat, :lng, keyword_init: false)
Loose.new(lat: 1, lng: 2).lat # => {lat: 1, lng: 2}go deeper
Recall that Struct.new returns a class with readers and writers per member, that missing members are nil and that extra positional arguments raise ArgumentError.
Explain the keyword_init: nil, true and false modes, the Ruby 3.2 change that made keywords work by default, and why keyword_init: false turns keywords into one Hash value.
Show when a mutable, nil-tolerant Struct is acceptable and when incomplete construction should instead be impossible, and point to the block form over subclassing Struct.new.
Weigh Struct as a quick internal carrier against the cost of mutable, partially filled records leaking across module boundaries, and set a team default for new record types.
## What Struct.new builds `Struct.new` is a **class generator**. It does not return a record; it returns a brand-new class whose superclass is `Struct`, and you normally assign it to a constant: ```ruby GpsFix = Struct.new(:lat, :lng, :recorded_at) ``` The generated class gets, for every **member** name: - a **reader** (`lat`) and a **writer** (`lat=`), so instances are mutable; - index access through `[]` and `[]=`, by member name or by position; - shared behaviour from `Struct`: `members`, `to_h`, `to_a` (alias `values`), `size`, `each`, `each_pair`, `values_at`, `dig`, a value-based `==`, and everything `Enumerable` adds. If the first argument is a String instead of a Symbol, the class is also registered as a constant under `Struct` (`Struct.new("Fix", :lat)` defines `Struct::Fix`). Most code passes only symbols and names the class by assigning it. ## Positional construction The generated class has its own `new` (and `[]`, an alias). With positional arguments it behaves like this: 1. Arguments fill members **in definition order**. 2. Members you leave off the end are set to **`nil`**; nothing raises. 3. Passing **more** arguments than members raises `ArgumentError` with the message `struct size differs`. 4. `new` with no arguments gives an instance whose members are all `nil`. So `GpsFix.new(52.52, 13.405).recorded_at` is `nil`. That forgiving default is convenient for records built up step by step and dangerous for values that must be complete. ## Keyword construction and keyword_init: Before Ruby 3.2 you needed `keyword_init: true` to build a Struct from keywords. Since 3.2 every Struct class accepts **both** styles by default, and in Ruby 4.0 the option only narrows what is accepted: | `keyword_init:` | `Fix.new(1, 2)` | `Fix.new(lat: 1, lng: 2)` | `Fix.keyword_init?` | |---|---|---|---| | omitted or `nil` | positional | keywords | `nil` | | `true` | `ArgumentError` (wrong number of arguments) | keywords | `true` | | `false` | positional | whole Hash lands in `lat`, `lng` is `nil` | `false` | Two details matter in reviews: - An **unknown keyword** raises `ArgumentError` (`unknown keywords: alt`), so typos surface at construction. - With `keyword_init: false`, a keyword call raises **nothing**: the keywords arrive as one positional Hash and become the first member's value. It is a silent bug, not an error. With the default (`nil`) a single **positional** Hash is also just a value: `Fix.new({lat: 1})` stores that Hash in `lat`. Only real keyword arguments switch to keyword filling. RuboCop's `Style/RedundantStructKeywordInit` cop (disabled by default) flags `keyword_init: nil` and `keyword_init: true` as redundant on Ruby 3.2 and later, and its own documentation notes that `true` is not purely redundant because it still forbids positional calls. ## Reading members back - `GpsFix.members` and `fix.members` return the member names as an Array of Symbols, in definition order. - `fix.to_h` returns a Hash such as `{lat: 52.52, lng: 13.405, recorded_at: nil}` (Ruby 3.4 and later print Symbol keys in that short form). With a block, `to_h` builds each pair from the block's two-element Array. - `fix[:lat]`, `fix["lat"]` and `fix[0]` read the same member; an unknown name raises `NameError`. ## Adding behaviour Pass a block to `Struct.new`; it is evaluated in the new class, so `def` inside it defines instance methods: ```ruby GpsFix = Struct.new(:lat, :lng) do def label = format("%.4f,%.4f", lat, lng) end ``` The older pattern `class GpsFix < Struct.new(:lat, :lng)` puts an extra **anonymous class** in the ancestors and fails with `TypeError` (superclass mismatch) if the file is loaded twice, because every `Struct.new` call returns a different class. RuboCop's `Style/StructInheritance` cop flags it. ## Mistakes interviewers listen for - Calling `Struct.new(:lat, :lng).lat`: the generator returns a **class**, so the call raises `NoMethodError` on the class; build an instance with `new` first. - Assuming a forgotten member raises: positional construction fills it with `nil`, and the error surfaces later wherever the `nil` is used. - Adding `keyword_init: true` out of habit on Ruby 3.2 and later: keywords already work, and the option now only removes positional calls. - Naming a member after an existing method (`count`, `hash`, `members`): the reader replaces it, which RuboCop's `Lint/StructNewOverride` cop reports. ## When Struct is the right tool Struct suits **mutable, loosely filled records**: parsed rows, intermediate results, small internal carriers. For values that must be complete and must not change after construction, the Ruby 3.2 `Data.define` generator is the better fit, because it requires every member and freezes its instances.
- What do `members` and `to_h` return for a Struct instance, and what does `to_h` do with a block?`members` returns the member names as Symbols in definition order, the same Array the class-level `members` returns. `to_h` returns a Hash of member name to value. Given a block, `to_h` calls it with each name and value and uses the two-element Array it returns as the key/value pair, raising `ArgumentError` if the block returns something else.
- Why does `class GpsFix < Struct.new(:lat, :lng)` raise when the file is loaded a second time?Each `Struct.new` call creates a new anonymous class. On the second load, `class GpsFix < ...` reopens the existing `GpsFix` with a different superclass, so Ruby raises `TypeError` (superclass mismatch). Assigning `GpsFix = Struct.new(:lat, :lng) do ... end` avoids the extra ancestor, which is why RuboCop's `Style/StructInheritance` flags the subclass form.
saying these in an interview costs you the question
- Struct.new(:lat, :lng) returns a record object you can read lat from.
- Leaving out a Struct member at construction raises ArgumentError.
- Keyword construction of a Struct still needs keyword_init: true in Ruby 4.0.
- With keyword_init: false, a keyword call raises an error.
- Subclassing Struct.new is the idiomatic way to add methods to it.