In Ruby, what must a Playlist class define to include Enumerable, and which methods does it get for free?
answer
- one method is the contract
- each yields every element once
- map, select, sort, min, lazy, each_slice
- results come back as Arrays
- no size, [], last or empty?
basics
~20 sDefine each so it yields every element, then include Enumerable. Every Enumerable method (map, select, reject, find, include?, reduce, sort_by, min_by, first, to_a, each_slice, group_by, tally, lazy) is built on that each; collection results come back as Arrays.
solid answer
~40 s`Enumerable` is a mixin whose only requirement is an instance method `each` that yields each element. After `include Enumerable`, a `Playlist` gains about sixty methods written in terms of `each`: `map`, `select`/`filter`, `reject`, `find`, `include?`, `count`, `reduce`, `sum`, `sort`, `sort_by`, `min`, `max`, `min_by`, `first`, `take`, `to_a`, `each_with_index`, `each_slice`, `group_by`, `partition`, `tally`, `zip`, `uniq`, `lazy` and more. Two limits matter in an interview: methods that build a collection return a plain **Array**, not a Playlist; and Enumerable gives **no** `size`, `length`, `[]`, `last` or `empty?`, because those need knowledge `each` cannot provide cheaply. `sort`, `min` and `max` additionally need the elements to answer `<=>`.
code
ruby · 22 linesTrack = Data.define(:title, :seconds)
class Playlist
include Enumerable
def initialize(tracks = [])
@tracks = tracks.dup
end
def each
return enum_for(:each) unless block_given?
@tracks.each { |track| yield track }
self
end
end
list = Playlist.new([Track.new("Intro", 95), Track.new("Theme", 240), Track.new("Coda", 130)])
p list.sum { |t| t.seconds } # => 465
p list.max_by { |t| t.seconds }.title # => "Theme"
p list.select { |t| t.seconds < 200 }.class # => Array
p list.respond_to?(:size) # => falsego deeper
Recall the recipe: define each that yields every element, include Enumerable, and methods like map and select appear.
Explain that every Enumerable method is built on each, that collection results are Arrays, and which methods such as size and last are absent.
Decide which Enumerable results should return the domain type, add a cheap size when you can, and check element requirements like <=> before relying on sort or max.
Use the one-method contract to give domain collections a familiar API while keeping storage and mutation rules private to the class.
## The contract: one method `Enumerable` is a module in Ruby's core. A class opts in with `include Enumerable` and by defining **`each`**, which must call the block once per element: ```ruby Track = Data.define(:title, :seconds) class Playlist include Enumerable def initialize(tracks = []) = (@tracks = tracks.dup) def each return enum_for(:each) unless block_given? @tracks.each { |track| yield track } self end end ``` Every Enumerable method is implemented by calling `each` with its own block. In CRuby they call `each` through `rb_block_call(obj, id_each, ...)` in `enum.c`, so they work on arrays, ranges, hashes, IO objects and your Playlist alike. ## What the Playlist gains | Kind | Methods (a selection) | |---|---| | Querying | `include?`/`member?`, `any?`, `all?`, `none?`, `one?`, `count`, `tally` | | Fetching | `first`, `take`, `take_while`, `drop`, `find`/`detect`, `find_index`, `min`, `max`, `min_by`, `max_by` | | Filtering | `select`/`filter`, `reject`, `filter_map`, `grep`, `partition`, `uniq`, `compact` | | Transforming | `map`, `flat_map`, `zip`, `each_with_object`, `to_a`, `to_h`, `sum`, `reduce` | | Ordering | `sort`, `sort_by`, `minmax`, `minmax_by` | | Iterating | `each_with_index`, `each_slice`, `each_cons`, `reverse_each`, `cycle`, `each_entry` | | Chaining | `lazy`, `chain`, `chunk_while`, `slice_when` | So `playlist.sum { |t| t.seconds }`, `playlist.max_by { |t| t.seconds }`, `playlist.each_slice(10).to_a` and `playlist.lazy.select { ... }.first(3)` all work with no further code. ## What it does not gain The missing methods are the ones `each` cannot answer without walking everything or that imply ordered, indexed storage: - **`size` / `length`**: Enumerable has `count`, which iterates every time. Define `size` yourself if the class knows it cheaply; Enumerable's own Enumerators will use it. - **`[]`, `last`, `empty?`**: not in Enumerable. Use `first`, `to_a.last`, `none?`, or define them. - **Mutation**: no `<<`, `delete` or `push`. The class decides which changes are allowed. ## Return types Collection-building methods return an **Array**: ```ruby playlist.select { |t| t.seconds < 240 }.class # => Array playlist.sort_by { |t| t.title }.class # => Array ``` If callers need a Playlist back, add explicit methods such as `def short = Playlist.new(select { |t| t.seconds < 240 })`. Enumerable will not wrap results for you. ## Requirements on the elements Some gained methods depend on what the **elements** can do: 1. `sort`, `min`, `max` and `minmax` compare elements with `<=>`. 2. `include?` compares with `==`. 3. `uniq` and `tally` use the elements as Hash keys. 4. `sum` without a block adds elements with `+`, starting from `0`. A `Track` built with `Data.define` has `==` and hashing, but not `<=>`, so `playlist.max` raises `ArgumentError`, while `playlist.max_by { |t| t.seconds }` works. ## Why it is designed this way Keeping the contract to one method means any object that can walk its elements, however it stores them (an array, a linked list, a database cursor, a file), gets the whole vocabulary. That is also why Enumerable avoids methods that assume cheap indexing or a known size. ## Key points - Define `each` that yields every element, then `include Enumerable`. - You get dozens of methods, all built on `each`. - Collection results are Arrays; no `size`, `[]`, `last` or `empty?`. - Ordering methods need `<=>` on the elements.
- Why does Enumerable offer count but not size?`count` can always be answered by walking `each`, so Enumerable provides it. `size` implies the object knows its length without iterating, which only the class can promise. If Playlist defines `size`, Enumerable's Enumerators call it to report their own sizes.
- How would you make playlist.select return a Playlist instead of an Array?Enumerable always builds Arrays, so define the method yourself: `def select(&block) = Playlist.new(super)` or a named method such as `short_tracks`. Overriding core names changes expectations for other developers, so a named method is often clearer.
saying these in an interview costs you the question
- Including Enumerable requires defining map, select and each
- Enumerable's select on a Playlist returns a Playlist
- Enumerable provides size and last once each is defined
- include Enumerable is enough; each is inherited from Object
- sort works on any Playlist regardless of what its elements are