RyanScottLewis
RyanScottLewis

Reputation: 14046

Setting an instance variable from a block

How would I achieve something like below so that when I set the s variable within the block, it also sets the @subject instance variable in my Topic class?

class Topic
  def subject(&blk)
    blk.call(@subject) if block_given?
    @subject unless block_given?
  end
end

my_topic = Topic.new

p my_topic.subject #=> nil

my_topic.subject do |s|
  s = ['one', 'two', 'three']
  s.pop
  p s #=> ['one', 'two']
end

p my_topic.subject #=> nil... want it to be ['one, 'two']

Upvotes: 4

Views: 2692

Answers (3)

Konstantin Haase
Konstantin Haase

Reputation: 25994

In addition to the given solutions, if you know the ivar is going to stay String/Array/Hash, whatever, you could do the following:

class Topic
  def subject
    @subject ||= 'sane default'
    if block_given? then yield(@subject)
    else @subject
    end
  end
end

t = Topic.new
t.subject { |s| s.replace 'fancy stuff' }

Though from what I guess you are doing, this is the most appropriate code:

class Topic
  def subject
    return @subject unless block_given?
    @subject = yield(@subject)
  end
end

t = Topic.new
t.subject { |s| 'fancy stuff' }
t.subject { |s| "very #{s}" }
t.subject # => "very fancy stuff"

Also, you could actually do that without a block:

class Topic
  def subject(value = nil)
    @subject = value % @subject if value
    @subject = yield @subject if block_given?
    @subject
  end
end

t = Topic.new
t.subject 'fancy stuff'                   # => "fancy stuff"
t.subject 'very %s'                       # => "very fancy stuff"
t.subject { |s| s.sub 'fancy', 'freaky' } # => "very freaky stuff"

Keep in mind that the statement p s you are using returns nil.

Upvotes: 2

Chuck
Chuck

Reputation: 237110

You can't do it the way you want. The block argument references the same object as the instance variable, but they are completely different variables and setting one will never set the other. There are two options:

  1. Set the variable to the result of the block, so it would be like:

    class Topic
      def subject
        @subject = yield if block_given?
        @subject unless block_given?
      end
    end
    

    and inside the block:

    my_topic.subject do
      s = ['one', 'two', 'three']
      s.pop
      p s #=> ['one', 'two']
      s
    end
    
  2. Have the subject method instance_eval the block so the block can set the instance variable explicitly

Upvotes: 4

sepp2k
sepp2k

Reputation: 370435

What you want to do is called pass-by-reference. That's not possible in ruby. You have two alternatives:

a) Do @subject = blk.call and return s from the block. Usually the simplest and cleanest option.

b) Instead of s = do @subject = in the block and then use instance_eval(&blk) instead of blk.call. This will set the @subject variable, however it requires the user of the subject method to know about the @subject variable and it doesn't allow you to call the block multiple times to set different variables.

Upvotes: 3

Related Questions