0

How can I define a variable that has shared access by instance methods of a Struct?

I can use a global variable $tmp like this:

Triple = Struct.new :x, :y, :z do 
  def mul s 
    $tmp.x = x * s
    $tmp.y = y * s
    $tmp.z = z * s    
    return $tmp     
  end   
end

$tmp = Triple[0.0, 0.0, 0.0]

one = Triple[1.0, 1.0, 1.0]
puts $tmp
puts one
one.mul 7.5
puts $tmp
puts one

:and:

$ /opt/src/ruby-3.3.5/bin/ruby --yjit -W0 example.rb
#<struct Triple x=0.0, y=0.0, z=0.0>
#<struct Triple x=1.0, y=1.0, z=1.0>
#<struct Triple x=7.5, y=7.5, z=7.5>
#<struct Triple x=1.0, y=1.0, z=1.0>

But I only want the variable to be accessible within Struct Triple, something like this:

Triple = Struct.new :x, :y, :z do 

  @tmp = Triple[0.0, 0.0, 0.0]

  def mul s 
    @tmp.x = x * s
    @tmp.y = y * s
    @tmp.z = z * s    
    return @tmp     
  end   
end

:but:

$ /opt/src/ruby-3.3.5/bin/ruby --yjit -W0 example.rb
example.rb:4:in `block in <main>': uninitialized constant Triple (NameError)

  @tmp = Triple[0.0, 0.0, 0.0]
         ^^^^^^
3
  • (Newbie) Would it be possible using a class variable rather than class instance variable? Would it be possible with a Class rather than Struct? (If so please post this example using that approach.) Commented Sep 11, 2024 at 0:48
  • After reading your code again I'm actually not sure what you are trying to achieve. In your first example, $tmp gets set to the result of the last multiplication. Why not return the multiplication result as a new Triple from mul and assign it outside the struct via $tmp = one.mul 7.5? Or is that what you are trying to do? If not, can you explain your intent? Commented Sep 11, 2024 at 6:19
  • First my concern to make tmp private may well be foolish given that mul returns tmp. Second that concern may not a Ruby concern but something that would be done in a different language. Third, optimization. Yes new Triple everywhere already works, at a cost. Commented Sep 11, 2024 at 16:29

2 Answers 2

2

While I am very unclear on your intentions here, you can accomplish your goal by assigning an instance variable inside a class instance method definition

Triple = Struct.new :x, :y, :z do  
  def self.tmp 
    @tmp ||= new(0,0,0)
  end

  def tmp = self.class.tmp
  
  def mul s 
    tmp.x = x * s
    tmp.y = y * s
    tmp.z = z * s    
    tmp     
  end   
end

This will result in

one = Triple[1.0, 1.0, 1.0]
#=> #<struct Triple x=1.0, y=1.0, z=1.0>
one.tmp
#=> #<struct Triple x=0.0, y=0.0, z=0.0>
one.mul 7.5
#=> #<struct Triple x=7.5, y=7.5, z=7.5>
one
#=> #<struct Triple x=1.0, y=1.0, z=1.0>
one.tmp
#=> #<struct Triple x=7.5, y=7.5, z=7.5>
Triple.tmp
#=> #<struct Triple x=7.5, y=7.5, z=7.5>
Sign up to request clarification or add additional context in comments.

3 Comments

OK please describe what this does -- @tmp ||= new(0.0,0.0,0.0)
@igouy it is equivalent to @tmp = @tmp || Triple.new(0.0,0.0,0.0). So if @tmp is nil which it is upon declaration then we set it equal to a new instance of the class, the new part is technically self.new. After than @tmp is set so it will return the instance created because the only things in ruby that are "falsey" are false and nil. The docs call this Abbreviated Assignment
smnky, I had not heard of endless methods. I now understand it is an experimental addition to v3.
1

Using a class instance method to hold the result of the last calculation isn't much better than having a global variable. You effectively replace $tmp with Triple.tmp. In both cases, you have some kind of global state which can change in unexpected ways. Any Triple#mul call anywhere in your codebase can alter that one value.

May I suggest two alternative approaches.

You could return a new Triple instance from your mul method:

Triple = Struct.new(:x, :y, :z) do
  def mul(s)
    Triple[x * s, y * s, z * s]
  end
end

one = Triple[1.0, 1.0, 1.0]
tmp = one.mul(7.5)

tmp
#=> #<struct Triple x=7.5, y=7.5, z=7.5>

If you prefer to re-use existing objects, you could add a mul variant that modifies the receiver instead of returning a new instance, let's call it mul!:

Triple = Struct.new(:x, :y, :z) do
  def mul!(s)
    self.x *= s
    self.y *= s
    self.z *= s
    self
  end
end

t = Triple[1.0, 1.0, 1.0]
t.mul!(7.5)

t
#=> #<struct Triple x=7.5, y=7.5, z=7.5>

The ! suffix is just a naming convention to indicate the "more dangerous" method, assuming that both methods might be present.

6 Comments

"isn't much better than having a global variable" I know. "re-use existing objects" is considerably faster. "modifies the receiver" breaks the calculation.
I could also thread the state through the method calls. So, define a method local variables to hold scratch instances of Triple which are passed as arguments to inner method calls. How would that work with a binary method like "-" ?
@igouy maybe you could outline the problem you are trying to solve. "breaks the calculation" – the example in your question only shows a single operation and it's not clear why you need that temporary variable in the first place. Can you provide more detail or show the actual calculation? Regarding "faster" / "more efficient" – always measure before optimizing. Creating objects isn't that expensive, Ruby was built for it. For most real-world problems, object allocation isn't the bottleneck.
See n-body Ruby yjit #7 705 secs and re-use existing objects ~480 secs.
@igouy that's why context is important. You didn't mention the problem's scale at all in your question ;) Running the given script with an argument of 50000000 creates a whopping 1,750,000,000 Triple instances, even though your 5 "bodies" consist of only 10 Triples in total. Of course that will have a huge performance impact.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.