Type checking in Crystal

493 Views Asked by At

I already asked a related question here and the answer did fix my issue but I have a more general misunderstanding of how crystal checks types since I keep running into similar issue so if someone could help me figure this out I'd be greatful. I've tried many things that would work great with Ruby but are absolutely not working with Crystal (I know they have many differences but I'm more familiar with Ruby).

Here is a class:

class Narray(T)
  getter shape
  getter values

  @shape : Tuple(Int32, Int32) | Nil

  def initialize(values : T)
    @values = values
    @type = T
    @shape = set_shape
  end

  def set_shape
    if (@type == Array(Array(Int32))) || (@type == Array(Array(Float64)))

      return {values.size, values[0].size}      # Line causing the error

    elsif (@type == Array(Int32)) || (@type == Array(Float64))
      return {1, @values.size}
    end
  end

  def is_matrix?
    if @values[0].is_a?(Array(Int32)) || @values[0].is_a?(Array(Float64))
      return true
    else
      return false
    end
  end
end

To define an array, I should do:

arr1 = Narray.new([1,2,3])

Which would run through set_shape to retrieve the shape of the array (a tuple representing the number of rows and number of columns). In this method, I check in the if statement if it's a 2D array or not. However, when running the line above, I get this error:

in script.cr:16: undefined method 'size' for Int32

      return {values.size, values[0].size}

Which is exactly what the first if statement should avoid. Since I'm initializing a 1D array, it shouldn't go through the .size part.

I'm pretty sure it's a trivial explanation, and that there is something pretty basic that I didn't get but I'd like to get it instead of stumbling upon this issue all the time.

How can I check for type in Crystal since the way I'm using isn't the right way ? I've tried using is_a?, == Type, using the is_matrix? method (which works fine and does the job of determining if it's 2D or 1D but still runs through the wrong part).

EDIT: according to the first answer, I've changed the set_shape method:

  def set_shape
    if (@values.is_a? Array(Array(Int32))) || (@values.is_a? Array(Array(Float64)))
      return {values.size, values[0].size}
    elsif (@values.is_a? Array(Int32)) || (@values.is_a? Array(Float64))
      return {1, @values.size}
    end
  end

But I still have the exact same error

2

There are 2 best solutions below

2
On BEST ANSWER

The problem you have is that you indirectly test for @values type, using an instance variable that happens to be the generic T. The compiler ain't that smart, and there is a possibility, at runtime, that @type changes and no longer reflects @values type. Even if you know it won't, Crystal wants to be safe (otherwise the program segfaults). Crystal complains because you assume a type that it won't acknowledge.

Drop @type. It's not helping and makes things more complicated than they have to. You already use a generic, now you should ask @values for what it is, then act accordingly. Crystal will like that, because you ascertain that type will be this and nothing else.

That being said, maybe you should have 2 different types to denote Array vs Array(Array). Maybe your code would be easier to deal with?

0
On

The following code works:

class Narray(T)
  getter shape
  getter values

  @shape : Tuple(Int32, Int32) | Nil

  def initialize(@values : T)
    @shape = set_shape
  end

  def set_shape
    values = @values # => asign to local var
    if values.is_a?(Array(Array(Int32))) || values.is_a?(Array(Array(Float64)))
      return {values.size, values[0].size}
    elsif (T == Array(Int32)) || (T == Array(Float64))
      return {1, values.size}
    end
  end

  def is_matrix?
    if @values[0].is_a?(Array(Int32)) || @values[0].is_a?(Array(Float64))
      return true
    else
      return false
    end
  end
end

arr1 = Narray.new([[1, 2], [3, 4]])
pp arr1.set_shape # => {2,2}
pp arr1.is_matrix? # => true