How to reference the address of a two dimensional array's element correctly?

I have already asked that question on StackOverflow forum, but it seems that most Crystal programmers are here. I am just trying to understand how to work with pointers in Crystal, I know they are “not safe”, and I don’t need that explanation since I have a C programming background.

So the example is gonna be the same, I need to point the element [x][y] of the array (which is of type Room). I have modified my code according to the information provided by Johannes Müller. Now my method for getting the address of an element looks like:

def map_ptr(x : Int32, y : Int32)
  p = @map.to_unsafe[x].to_unsafe + y
  pointerof(p)
end

The type it gives me is a Pointer(Pointer(Room), which seems odd. The fact that I point to a local variable seems also wrong, but if I do not pass it as a variable I get the error:

Error: can't take address of @map.to_unsafe[x].to_unsafe + y

However, I can not dereference the above pointer correctly. When I type:

current_room = map_ptr(0, 0)
current_room.value.value.my_method

then error says:

Error: undefined method 'value' for Room (compile-time type is (Pointer(Room) | Room))

but saying current_room.value.my_method is also wrong as it gives me:

Error: undefined method 'my_method' for Pointer(Room)

This seems like a never ending loop, what did I wrong?

I have find out that the redundant pointer type was caused by taking a pointerof of @map.to_unsafe[x].to_unsafe + y which is a pointer already. In stead that function appears to be more like:

def map_ptr(x : Int32, y : Int32)
  @map.to_unsafe[x].to_unsafe + y
end

However, that gives me a memory access error while trying to access it’s value like:

current_room = map_ptr(0, 0)
current_room.value.my_method

Is map empty? Are any of the arrays inside it empty? Dereferencing the first element of an empty array would definitely cause this.

That almost seems like it… but I have no idea how it is possible since I am filling the @map in the initialization, before actually calling the function that is accessing it.

I have now initialized the map with nil before actually filling it, and I see that the first element is of type Nil which gives me a compile time error:

Error: undefined method 'my_method' for Nil (compile-time type is (Room | Nil))

Am I still doing something wrong with the array initialization? Here is the initialization code:

@map = Array(Array(Room?)).new(SIZE) { Array(Room?).new(SIZE, nil) }
    
(0...SIZE).each do |x|
  (0...SIZE).each do |y|
    room_type = ROOMS.sample
    @map[x][y] = Room.new(room_type)
  end
end

I guess whatever is wrong with your program lies outside the limited code you have shared.
Can you give a bigger picture example?

Also, what are you really trying to do? As I already mentioned on SO, taking a pointer of an array item is very unsafe.
If you just take a pointer to immediately dereference it and call a method, what do you need the pointer for? You can just access the array element directly.

I’ve also got this question, but I figured the OP has their reasons. They mentioned they recognize it’s not safe.

However, even if we ignoring the safety component, avoiding direct pointer dereferencing will probably indicate what’s actually going wrong because it involves operating on top of meaningful abstractions.

Ya, in that case it would not make sens. The above code is merely a simplified example. I see I have to provide more context, as this just seems to confuse people no less then it is confusing me. So I cut all that pointer non-sens as it seems to be completely irrelevant and is just distracting from the actual problem.

The minimal version of the Room class, required for this context, may be like this:

require './meta'

class Room
  include RubyClass                                                
  attr_reader :type, :x, :y
  
  def initialize(type = "", x = 0, y = 0)
    @type = type
    @x = x
    @y = y
  end
end

The RubyClass module simply contains a macro to define the Ruby like attr_reader.

module RubyClass
  macro attr_reader(*args)
    {% for arg in args %}
      def {{arg.id}}
        @{{arg.id}}
      end 
    {% end %}
  end
end

Now after the map initialization, let’s say I want to print each room object in a loop:

(0...SIZE).each do |x|
  (0...SIZE).each do |y|
    # Returns an object of type Room
    puts @map[x][y]
  end
end

This works and prints the correct type and object id:

#<Room:0x7fe59e745d60>
#<Room:0x7fe59e745d40>
#<Room:0x7fe59e745d20>
#<Room:0x7fe59e745d00>
#<Room:0x7fe59e745ce0>
#<Room:0x7fe59e745cc0>
#<Room:0x7fe59e745ca0>
#<Room:0x7fe59e745c80>
#<Room:0x7fe59e745c60>
...

However, when I try to print an instance of the Room:

(0...SIZE).each do |x|
  (0...SIZE).each do |y|
    # Compile time error !!!
    puts @map[x][y].y
  end
end

I get a compile time error:

Error: undefined method 'y' for Nil (compile-time type is (Room | Nil))

Can somebody explain what is going on with that type variation? Here is a minimal reproducible example.

EDIT:
I use Crystal 1.6.2 compiled at x86_64-pc-linux-gnu with LLVM 14.0.6

You could do this:

puts @map[x][y].as(Room).y

But would probably prefer something like this instead:

puts @map[x][y].try(&.y)

Also, why not just use Object#getter? It’s pretty much equivalent to attr_reader.

1 Like

Here is my problem; one of my condition was looking like:

(accept = player.move(0, -1, @map)) &&
            (current_room = current_room.value.north)

So first player will check if he can move to a given room and change it’s position. The move function will return true or false, depending if it can move to a given position or not.

In this case compiler have forced at me to use current_room.try(&.value).try(&.north)), even when there is no possibility for the current_room.value.north to be nil.

That seems like a bug to me, or a very paranoid type system at best.

That is exactly what I was trying to avoid, since I thought that requires me to support Pointer(Room | Nil) | Nil kind of type. However that seems like my pointer is actually returning the correct type (Pointer(Room | Nil)) since it does never return nil. Nevertheless, thank you, that helped me to debug my code.

Yes, I know that macro. I guess I just like to have something familiar around and I am an unconditional Ruby lover :blush:

Thanks for sharing the reproducible example, that explains it:

You declare the type of @map as Array(Array(Room?)).

@map = Array(Array(Room?)).new(SIZE) { Array(Room?).new(SIZE, nil) }

That type allows the inner array to contain nil values which you then have to consider whenever you fetch an element. I figure that type is simply incorrect. You don’t want to have Nil values there, only Room.

This is an example for how you can improve this and also shorten the entire array construction code:

    @map = Array(Array(Room)).new(SIZE) { |x|
      Array(Room).new(SIZE) { |y|
        room_type = ROOMS.sample
        Room.new(room_type, x, y)
      }
    }

https://carc.in/#/r/eegz

2 Likes

I think most people here love Ruby. That’s how Crystal came to be in the first place.
But not everything in Ruby is great, so Crystal tries to make some things better, even if your used to the Ruby way. And when in Rome, do as the Romans do. It’ll be better for you and for anyone who’s trying to read your code when it uses Crystal idioms instead of custom Ruby-like idioms.

Thank you. That makes sens now. So, completely not related to what I was assuming. When I apply that to my code there is no need to check for each element being nil.

Yes, I will consider changing that when I start sharing my code. Right now I just pick up Crystal over the weekend and wanna check how it stands against C.

It took me a few days to get how some things works, but I have not learned C over a day either, so that’s fair. I just assumed it’s gonna be much more like Ruby, and that was a bit misleading.

2 Likes