views:

657

answers:

2

I want to define a class method that has access to a local variable. So this would be different for each instance of the class. I know you can make a class method dynamic with lambda like when you use it with named_scope. But can this be done for values that are specific to an instance?

In detail it is the has_attached_file method for the paperclip plugin in rails. I want to pass a lambda for the styles hash so that the image styles can be based off of attributes of the object stored in the DB. Is this possible?

+8  A: 

Ok, you're being unclear.

Local variables in ruby begin with a lowercase letter (like foo, bar, or steve), and are lexically scoped (like C variables). They have nothing to do with "an instance of a class"

Instance variables in ruby begin with an @ sigil (like @foo, @bar, or @carl), and are in scope whenever the current value of self is the object they are stored in.

If you want a method that can access the instance variables of an object directly, that's called an instance method. For example, battle_cry and initialize are both instance methods:

class Character
  def initialize(name)
    @name=name
  end
  def battle_cry
    @name.upcase + "!!!"
  end
  def Character.default
    new("Leeroy Jenkins")
  end
end

A class method, by contrast, is a method for a Class object, and doesn't have access to any of the instance variables of that object. In the above example, default is a class method.

If you want a (class or instance) method that triggers a change in or gets a value from the current scope, ruby uses a type of callback called a block.

class Character
   ATTACKS = [ "Ho!", "Haha!", "Guard!", "Turn!", "Parry!", "Dodge!", "Spin!", "Ha", "THRUST!" ]
   def attack
     ATTACKS.inject(0) { |dmg, word| dmg + yield(word) }
   end
end

person = Character.default
puts person.battle_cry

num_attacks = 0;
damage = person.attack do |saying|
  puts saying
  num_attacks += 1
  rand(3)
end
puts "#{damage} points of damage done in #{num_attacks} attacks"

In the above example, attack uses the yield keyword to call the block passed to it. When we call attack, then, the local variable num_attacks is still in scope in the block we pass it (delimited here by do ... end), so we can increment it. attack is able to pass values into the block, here they are captured into the saying variable. The block also passes values back to the method, which show up as the return value of yield.

The word lambda in ruby usually means the lambda keyword, which is used to make blocks into freestanding, function like objects (which themselves are usually referred to as lambdas, procs, or Procs).

bounce = lambda { |thing| puts "I'm bouncing a #{thing}" }
bounce["ball"]
bounce["frog"]

So I think what you're asking is whether you can pass a Proc in place of a Hash for an argument to a method. And the answer is "it depends". If the method only ever uses the #[] method, then yes:

class Character
  attr_accessor :stats
  def set_stats(stats)
    @stats = stats
  end
end

frank = Character.new("Victor Frankenstein")
frank.set_stats({ :str => 7, :dex => 14, :con => 9, :int => 19, :wis => 7, :cha => 11 })

monster = Character.new("Frankenstein's Monster")
monster.set_stats(lambda do |stat_name|
  rand(20)
end)

However, it might use some other Hash specific methods, or call the same key multiple times, which can produce weird results:

monster = Character.new("Frankenstein's Monster")
monster.set_stats(lambda do |stat_name|
  rand(20)
end)

monster.stats[:dex] #=> 19
monster.stats[:dex] #=> 1

In which case, you may be better off caching the requests in an intermediate hash. This is fairly easy, since a Hash can have an initializer block. So if we change the above to:

monster.set_stats(Hash.new do |stats_hash, stat_name|
  stats_hash[stat_name] = rand(20)
end)

monster.stats[:dex] #=> 3
monster.stats[:dex] #=> 3

The results are cached in the hash

To see more about Hash block initializers, see ri Hash::new:

-------------------------------------------------------------- Hash::new
     Hash.new                          => hash
     Hash.new(obj)                     => aHash
     Hash.new {|hash, key| block }     => aHash
------------------------------------------------------------------------
     Returns a new, empty hash. If this hash is subsequently accessed
     by a key that doesn't correspond to a hash entry, the value
     returned depends on the style of new used to create the hash. In
     the first form, the access returns nil. If obj is specified, this
     single object will be used for all default values. If a block is
     specified, it will be called with the hash object and the key, and
     should return the default value. It is the block's responsibility
     to store the value in the hash if required.

        h = Hash.new("Go Fish")
        h["a"] = 100
        h["b"] = 200
        h["a"]           #=> 100
        h["c"]           #=> "Go Fish"
        # The following alters the single default object
        h["c"].upcase!   #=> "GO FISH"
        h["d"]           #=> "GO FISH"
        h.keys           #=> ["a", "b"]

        # While this creates a new default object each time
        h = Hash.new { |hash, key| hash[key] = "Go Fish: #{key}" }
        h["c"]           #=> "Go Fish: c"
        h["c"].upcase!   #=> "GO FISH: C"
        h["d"]           #=> "Go Fish: d"
        h.keys           #=> ["c", "d"]
rampion
Wow, thanks for all the detail, this response is amazing!
bwizzy
+6  A: 

Disclaimer: First, the question (Can you pass self to lambda?) and the problem you're trying to solve (dynamic styles with paperclip) don't fully match up. I won't answer the original question because it's not entirely related to your problem, and rampion took a valiant stab at it.

I'll instead answer your paperclip question.

In detail it is the has_attached_file method for the paperclip plugin in rails. I want to pass a lambda for the styles hash so that the image styles can be based off of attributes of the object stored in the DB. Is this possible?

Yes, it is possible. In paperclip, the :styles option can take a Proc. When the attachment is initialized, if a Proc was used, the attachment itself is passed to the Proc. The attachment has a reference to the associated ActiveRecord object, so you can use that to determine your dynamic styles.

For example, your has_attached_file declaration might look something like this (assuming a User and avatar scenario where the user can customize the size of their avatar):

class User < ActiveRecord::Base
  has_attached_file :avatar, :styles => lambda { |attachment| 
    user = attachment.instance
    dimensions = "#{user.avatar_width}x#{user.avatar_height}#"
    { :custom => dimensions }
  }
end
Ryan McGeary
Thanks Ryan, that is exactly what I was looking for.
bwizzy
@bwizzy - you should click the check mark by Ryan's answer then, to note that (1) he answered your question and (2) to reward him for doing so.
rampion
Thanks, sorry this is my first post. Thanks for all the quick replies and great detail.
bwizzy