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

Type Range misidentified as Enumerable #4684

Closed
docelic opened this issue Jul 7, 2017 · 10 comments · Fixed by #10519
Closed

Type Range misidentified as Enumerable #4684

docelic opened this issue Jul 7, 2017 · 10 comments · Fixed by #10519
Labels
kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:compiler:generics

Comments

@docelic
Copy link
Contributor

docelic commented Jul 7, 2017

Hello,

Here is an example that demonstrates how, when Enumerable is present in the aliased type, a Range object no longer reports true for is_a?( Range).

require "yaml"

# This example demonstrates how Enumerable(Int32)
# being present in the alias type 'Virtual' makes
# a Range object no longer reporting true for
# is_a?( Range).

module X

  class Y
    alias Virtual = Range(Int32, Int32) | Enumerable(Int32)

    YAML.mapping({
      year: { type: Virtual, nilable: true},
    })
    
    def initialize() end
  end
end


#
v= X::Y.new
v.year= 2015..2017

value= v.year
puts "
value.inspect=#{value.inspect},
value.class=#{value.class},
value.is_a?( Range)=#{value.is_a? Range(Int32,Int32)}
value.is_a?( Range(Int32,Int32))=#{value.is_a? Range(Int32,Int32)}
Range===value=#{Range===value}
typeof(value)=#{typeof(value)}"

When the above code is run, the "puts" prints the following:

value.inspect=2015..2017,
value.class=Range(Int32, Int32),

# Should be true but isn't:
value.is_a?( Range)=false
value.is_a?( Range(Int32,Int32))=false
Range===value=false

# Instead it is recognized as:
typeof(value)=(Enumerable(Int32) | Nil)

The same code on play: https://play.crystal-lang.org/#/r/2bfo

Thanks

@straight-shoota
Copy link
Member

A Range(Int32, Int32) is also an Enumerable(Int32), so the union is actually condensed to just Enumerable(Int32) - whether you assign a Range or not.

Range(Int32, Int32) | Enumerable(Int32) # => Enumerable(Int32)

https://play.crystal-lang.org/#/r/2bft

That's why typeof(value) does not include Range. But is_a? should match the actual type at runtime.

@ysbaddaden
Copy link
Contributor

is_a? matches at compile time, not runtime.

@docelic
Copy link
Contributor Author

docelic commented Jul 7, 2017

Are you saying that there is no runtime way to detect that a Range is a Range? (When it is union typed with Enumerable)

@straight-shoota
Copy link
Member

No, this is exactly the purpose of is_a?:

The pseudo-method is_a? determines whether an expression's runtime type inherits or includes another type.

https://crystal-lang.org/docs/syntax_and_semantics/is_a.html

@ysbaddaden
Copy link
Contributor

The branch happens at runtime, but the definition happens at compile time, if I remember correctly. Maybe I'm wrong, or a bit confused, thought.

@konovod
Copy link
Contributor

konovod commented Jul 7, 2017

Reduced:

def test(v)
  if v.is_a? Range
    puts "Range!"
  else
    pp v, typeof(v), v.class
  end
end

puts "works for ordinary var"
test(rand < 1.5 ? (1..3) : [1,2,3])
puts "don't work for Enumerable"
test((rand < 1.5 ? (1..3) : [1,2,3]).as(Enumerable(Int32)))

https://carc.in/#/r/2bjq
Output:

works for ordinary var
Range!
don't work for Enumerable
v         # => 1..3
typeof(v) # => Enumerable(Int32)
v.class   # => Range(Int32, Int32)

Looks like a bug IMHO.

@docelic
Copy link
Contributor Author

docelic commented Jul 8, 2017

My workaround for the moment will be to do the comparison using v.class== Range(Int32,Int32).

@docelic
Copy link
Contributor Author

docelic commented Jul 8, 2017

It appears I can't use #class because it does not seem to restrict/guarantee type like === or value.is_a? does.

@konovod
Copy link
Contributor

konovod commented Jul 8, 2017

Even more reduced:

module A(T)
end

class B(T)
  include A(T)
end

class C(T)
  include A(T)
end

def test(v)
  if v.is_a? B
    puts "B!"
  else
    pp v, typeof(v), v.class
  end
end

puts "works for ordinary var"
test(rand < 1.5 ? B(Int32).new : C(Int32).new)
puts "don't work for Enumerable"
test((rand < 1.5 ? B(Int32).new : C(Int32).new).as(A(Int32)))

https://carc.in/#/r/2bmz
If modules are not generic - it works:
https://carc.in/#/r/2bn0
So generics are root of evil.

About workaround - i don't know. x.as(Range(Int32, Int32)) isn't working too. There is also unsafe_as but it looks like asking for segfault.

@straight-shoota straight-shoota added kind:bug A bug in the code. Does not apply to documentation, specs, etc. topic:compiler:generics labels Mar 14, 2021
@HertzDevil
Copy link
Contributor

OP's example for current Crystal versions:

require "yaml"

module X
  class Y
    alias Virtual = Range(Int32, Int32) | Enumerable(Int32)

    include YAML::Serializable

    property year : Virtual?
    
    def initialize
    end
  end
end

v = X::Y.new
v.year = 2015..2017

value = v.year
p! value.inspect                    # => "2015..2017"
p! value.class                      # => Range(Int32, Int32)
p! value.is_a?(Range)               # => false
p! value.is_a?(Range(Int32, Int32)) # => false
p! Range === value                  # => false
p! typeof(value)                    # => (Enumerable(Int32) | Nil)

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:compiler:generics
Projects
None yet
Development

Successfully merging a pull request may close this issue.

5 participants