Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Ruby: Disallow updating of an array that is a class variable

Tags:

arrays

class

ruby

I am writing a simple Tic Tac Toe game, in which I have a class for the Board, the Player, the Computer, and the Game itself. In my Board class, I have set a class variable @board (which is an array) as attr_reader, which should disallow writing directly to it. Although the following will not work (as intended)

game_board = Board.new 
game_board.board = "some junk"

The following does work, which I don't want to happen

game_board = Board.new
game_board.board[0] = "some junk"

How do I stop the class array variable @board from being written to? Current class code below:

class Board
  attr_reader :board

  def initialize
    create_board
  end

  private
  def create_board
    @board = Array.new(3).map{Array.new(3)}
  end
end

game_board = Board.new 
game_board.board
 #=> [[nil,nil,nil],[nil,nil,nil],[nil,nil,nil]]
game_board.board = "junk"
 #=> undefined method 'board ='  

game_board.board[0] = "junk" 
game_board.board 
 #=> ["junk",[nil,nil,nil],[nil,nil,nil]] #I don't want to allow this!

I tried googling this, but to no avail, however I am a complete beginner, so I may not be using the correct search terms

like image 284
AlexManning Avatar asked Apr 30 '16 21:04

AlexManning


2 Answers

I believe you need to make the array immutable.

You can use Array#freeze to achieve that.

Your code after that should look like :

class Board
  attr_reader :board

  def initialize
    create_board
  end

  private
  def create_board
    @board = Array.new(3).map{Array.new(3).freeze}.freeze
  end
end

On running your first example :

>> game_board = Board.new 
#<Board:0x00000001648b50 @board=[[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]]>
>> game_board.board = "some junk"
NoMethodError: undefined method `board=' for #<Board:0x00000001648b50>
    from (irb):14
    from /home/alfie/.rvm/rubies/ruby-2.1.3/bin/irb:11:in `<main>'

On running your second example :

>> game_board = Board.new
#<Board:0x00000001639e48 @board=[[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]]>
>> game_board.board[0] = "some junk"
RuntimeError: can't modify frozen Array
    from (irb):16
    from /home/alfie/.rvm/rubies/ruby-2.1.3/bin/irb:11:in `<main>'
like image 165
Alfie Avatar answered Oct 23 '22 02:10

Alfie


Defining attr_reader only, without attr_writer, will prevent assignments to the @board variable only. In other words, your Board class does not expose an interface to modify what's stored in @board, but does nothing to prevent modifications of the initial value.

You could use freeze:

def create_board
  @board = Array.new(3) { Array.new(3).freeze }
  @board.freeze
end

(also, you don't need map there)

Freezing the top-level array and the nested ones will do what you describe, but I guess it will also break your game because modifications will be completely impossible.

What I'd suggest is to not expose @board at all and consider it private. You should then expose an interface to set values in the board, and provide a method to return a readonly representation of the board.

class Board

  def initialize
    create_board
  end

  def []=(x, y, value)
    @board[x][y] = value
  end

  def board
    @board.map { |a| a.dup.freeze }.freeze
  end

  private

  def create_board
    @board = Array.new(3) { Array.new(3) }
  end
end


b = Board.new
b.board
# => [[nil, nil, nil], [nil, nil, nil], [nil, nil, nil]]

b[1,2] = "x"
b[0,0] = "o"
b.board
# => [["o", nil, nil], [nil, nil, "x"], [nil, nil, nil]]

b.board[0] = "junk"
# RuntimeError: can't modify frozen Array
b.board[0][1] = "junk"
# RuntimeError: can't modify frozen Array
b.board
# => [["o", nil, nil], [nil, nil, "x"], [nil, nil, nil]]
like image 36
tompave Avatar answered Oct 23 '22 02:10

tompave