Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Indexable itself cannot be used for Enumerable#zip #8608

Closed
lugia-kun opened this issue Dec 22, 2019 · 4 comments · Fixed by #8621
Closed

Indexable itself cannot be used for Enumerable#zip #8608

lugia-kun opened this issue Dec 22, 2019 · 4 comments · Fixed by #8621
Labels
kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:stdlib:collection

Comments

@lugia-kun
Copy link

With the following definition, it says that Enumerable#zip accepts Indexable (and objects include it), but Indexable itself cannot be used as an argument of Enumerable#zip, and getting error on the internal macro expansion.

crystal/src/enumerable.cr

Lines 1507 to 1546 in 8dd3271

# Yields elements of `self` and *others* in tandem to the given block.
#
# Raises an `IndexError` if any of *others* doesn't have as many elements
# as `self`. See `zip?` for a version that yields `nil` instead of raising.
#
# ```
# a = [1, 2, 3]
# b = ["a", "b", "c"]
#
# a.zip(b) { |x, y| puts "#{x} -- #{y}" }
# ```
#
# The above produces:
#
# ```text
# 1 -- a
# 2 -- b
# 3 -- c
# ```
#
# An example with multiple arguments:
#
# ```
# (1..3).zip(4..6, 7..9) do |x, y, z|
# puts "#{x} -- #{y} -- #{z}"
# end
# ```
#
# The above produces:
#
# ```text
# 1 -- 4 -- 7
# 2 -- 5 -- 8
# 3 -- 6 -- 9
# ```
def zip(*others : Indexable | Iterable | Iterator, &block)
Enumerable.zip(self, others) do |elems|
yield elems
end
end

I wanted to create a generic class which accept two any objects which can be used for #zip (with other data).

class Foo(T)
  def initialize(@a : Indexable(T) | Iterable(T) | Iterator(T), @b : Indexable(T) | Iterable(T) | Iterator(T))
  end
  
  def some_work
    @a.zip(@b) do |a, b|
      puts a, b
    end
  end
end
 
f = Foo.new([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
f.some_work

https://play.crystal-lang.org/#/r/88yn

Getting following error:

Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro 'macro_140238057131296'

Code in /usr/lib/crystal/enumerable.cr:1648:5

 1648 | {% begin %}
        ^
Called macro defined in /usr/lib/crystal/enumerable.cr:1648:5

 1648 | {% begin %}

Which expanded to:

 > 10 |           
 > 11 |             # Otherwise advance the iterator
 > 12 |             other_elem0 = iter0.next
                                  ^----
Error: undefined local variable or method 'iter0' for Enumerable(T):Module

I have not checked for Iterable nor Iterator.

@RX14
Copy link
Member

RX14 commented Dec 22, 2019

This is because Union(Foo, Bar) < Foo is false. I'm not sure if this should be true or if the logic in the macro is bad...

@asterite
Copy link
Member

Just define Foo like this:

class Foo(T)
  def initialize(@a : T, @b : T)
  end

  def some_work
    @a.zip(@b) do |a, b|
      puts a, b
    end
  end
end

f = Foo.new([1.0, 2.0, 3.0], [4.0, 5.0, 6.0])
f.some_work

@RX14
Copy link
Member

RX14 commented Dec 22, 2019

@asterite that's a workaround in this case, not a solution still. < in macros doesn't have the same semantics as is_a?, yet everyone seems to use it as such. Should macros have an is_a? analogue?

@lugia-kun
Copy link
Author

I noticed that this problem occurs only if the argument type is Union(Iterator, Indexable), Union(Iterator, Iterable) or Union(Iterator, Indexable, Iterable) (i.e., for Union(Indexable, Iterable), this problem does not occur).

a = [1, 2, 3]
if rand < 0.5
  b = {1.0, 2.0, 3.0} # Indexable
else
  b = Dir.open("/")   # Iterable but not Indexable
end
 
a.zip(b) do |x, y|
  puts x, y
end

https://play.crystal-lang.org/#/r/89qb

I don't know who mixes up Iterator and Indexable or Iterable, but it seems that a run-time type check is required for the following code:

a = [1, 2, 3]
if rand < 0.5
  b = {1.0, 2.0, 3.0} # Indexable
elsif rand < 0.5
  b = Dir.open("/")   # Iterable but not Indexable
else
  b = ['a', 'b', 'c'].each # Iterator
end
 
a.zip(b) do |x, y|
  puts x, y
end

https://play.crystal-lang.org/#/r/89a4

I just tried to implement Enumerable#zip to perform run-time (or compile-time if possible) type check:

module Enumerable(T)
  module Zipper
    def self.for(iter : Iterator(T)) forall T
      ZipIterator(T).new(iter)
    end
    
    def self.for(iter : Indexable(T)) forall T
      ZipIndexable(T).new(iter)
    end
    
    def self.for(iter : Iterable(T)) forall T
      ZipIterable(T).new(iter)
    end
  end
  
  struct ZipIndexable(T)
    def initialize(@container : Indexable(T))
    end
    
    def next(index_hint) : T
      @container[index_hint]
    end
  end
  
  struct ZipIterator(T)
    def initialize(@iter : Iterator(T))
    end
    
    def next(index_hint) : T
      if (n = @iter.next).is_a?(Iterator::Stop)
        raise IndexError.new
      else
        n
      end
    end
  end
 
  struct ZipIterable(T)
    @iter : Iterator(T)
    
    def initialize(iter : Iterable(T))
      @iter = iter.each
    end
 
    def next(index_hint) : T
      if (n = @iter.next).is_a?(Iterator::Stop)
        raise IndexError.new
      else
        n
      end
    end
  end
 
  def self.zip(main, others : U, &block) forall U
    {% begin %}
      iters = {
        {% for o, i in U %}
          Zipper.for(others[{{i}}]),
        {% end %}
      }
      puts typeof(iters) # To confirm the type has been reduced as possible as we can

      main.each_with_index do |m, i|
        elems = {
          m,
          {% for o, i in U %}
            iters[{{i}}].next(i),
          {% end %}
        }
       
        yield elems
      end
    {% end %}
  end
 
  def zip(*others : Indexable | Iterable | Iterator, &block)
    Enumerable.zip(self, others) do |elems|
      yield elems
    end
  end
end
 
a = [1, 2, 3]
if rand < 0.5
  b = {1.0, 2.0, 3.0} # Indexable
elsif rand < 0.5
  b = Dir.open("/")   # Iterable but not Indexable
else
  b = ['a', 'b', 'c'].each # Iterator
end
 
a.zip(b) do |x, y|
  puts({typeof(x), typeof(y)})
  puts({x, y})
end
 
puts
a.zip([1.0, 2, "a"], Dir.open(".")) do |x, y, z|
  puts({typeof(x), typeof(y), typeof(z)})
  puts({x, y, z})
end

https://play.crystal-lang.org/#/r/89qi

@asterite asterite added kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:stdlib:collection labels Dec 25, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:stdlib:collection
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants