Skip to content

Add Enumerator::Lazy signatures for the methods it overrides - #3285

Open
hasannadeem wants to merge 1 commit into
ruby:masterfrom
hasannadeem:enumerator-lazy-signatures
Open

hasannadeem wants to merge 1 commit into
ruby:masterfrom
hasannadeem:enumerator-lazy-signatures

Conversation

@hasannadeem

Copy link
Copy Markdown
Contributor

Enumerator::Lazy only declared compact, eager and force. Every other method it overrides fell back to Enumerable, so a lazy chain was typed as an Array after its first step, and calling force or eager on it was an error:

[1, 2].lazy.map { |n| n.to_s }                           # ::Array[::String]
[1, 2].lazy.map { |n| n.to_s }.force                     # Type `::Array[::String]` does not have method `force`
[1, 2].lazy.filter_map { |n| n.to_s if n.odd? }.eager    # Type `::Array[::String]` does not have method `eager`

With this change Steep infers ::Enumerator::Lazy[::String, void], ::Array[::String] and ::Enumerator[::String, void] for those three lines.

This adds the methods Enumerator::Lazy defines (Enumerator::Lazy.instance_methods(false)), except to_enum and enum_for:

  • map, flat_map, select, reject, filter_map, take, take_while, drop, drop_while, grep, grep_v, uniq, with_index and zip, plus the collect, collect_concat, filter and find_all aliases
  • chunk, chunk_while, slice_after, slice_before and slice_when, which Enumerable types as a plain Enumerator
  • lazy, which returns self

The parameters follow the Enumerable signatures, with some differences:

  • Only the block forms are declared where Lazy raises ArgumentError without a block (map, select, take_while, chunk_while, …).
  • The new lazy enumerator uses the default R, as Enumerable#lazy does. each with a block on a derived lazy enumerator returns nil, not the receiver's R.
  • take and drop take an int.
  • with_index takes an Integer? offset. nil means 0, and a non-Integer offset is yielded as the first index unconverted.
  • zip with a block is eager and returns nil, as in Enumerable#zip.

The doc comments are what rake annotate produces from Ruby v4.0.7's enumerator.c.

Tests: 18 of the new tests fail without the signature change. The tests for chunk, chunk_while, the slice_* methods and lazy pass either way, because a Lazy is also an Enumerator; the Steep check above is what shows the difference there. rake validate, rake typecheck_test and rake stdlib_test pass. The 16 stdlib_test errors on Ruby 3.4 also occur on master.

Lazy only declared compact, eager and force, so map, select, take and the
other methods that return a lazy enumerator were typed by Enumerable as
returning an Array, and calling force or eager on the result was a type
error.
Comment thread core/enumerator.rbs
#
def zip: [U] (_Each[U] arg) -> Enumerator::Lazy[[ E, U? ]]
| (_Each[untyped], *_Each[untyped]) -> Enumerator::Lazy[Array[untyped]]
| [U] (_Each[U]) { ([ E, U? ]) -> void } -> nil

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With a block, lazy_zip delegates to Enumerable#zip via rb_call_super, but this def replaces all of Enumerable#zip's overloads. So the block form with multiple arguments, which type-checks on master, would no longer match:

(1..3).lazy.zip([4], [5]) { |a| p a }  # => nil at runtime

How about copying the remaining block overload from Enumerable#zip?

Suggested change
| [U] (_Each[U]) { ([ E, U? ]) -> void } -> nil
| [U] (_Each[U]) { ([ E, U? ]) -> void } -> nil
| (_Each[untyped], *_Each[untyped]) { (Array[untyped]) -> void } -> nil

A test case for it in test_zip would be nice too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants