Forum: Ruby redefining @my_attr=

Announcement (2017-05-07): www.ruby-forum.com is now read-only since I unfortunately do not have the time to support and maintain the forum any more. Please see rubyonrails.org/community and ruby-lang.org/en/community for other Rails- und Ruby-related community platforms.
Mike C. (Guest)
on 2009-02-11 06:02
I want to write a generic validator that ensures an attr value is in a
list before assigning, and if not, assigns a default.  It works for
self.attrib='value', and Dummy.new.attrib='value', but not
@attrib='value' within the class.  Any way to do that?  Here's the code:

module Validator

  def validate_is_member_of( attrib, list, default )
    original_method = instance_method( "#{attrib}=".to_sym )
    define_method( "#{attrib}=".to_sym ) do |val|
      instance_variable_set( "@#{attrib}", (list.include?( val ) ? val :
default ))
    end
  end

end

class Dummy
  extend Validator

  attr_accessor :name, :type
  validate_is_member_of :type, [ :fruit, :veggie, :dairy ], :fruit

  def initialize( name, type )
    @name = name
    @type = type
  end
end

good = Dummy.new('good', :veggie )      # => ... @type=:veggie
good.type = :ice_cream                              # => ...
@type=:fruit.  i know i overwrote a valid value. it's ok.
bad = Dummy.new('bad', :chocolate )     # => ... @type=:chocolate; I
want :fruit here.
James B. (Guest)
on 2009-02-11 06:37
(Received via mailing list)
Mike C. wrote:
> I want to write a generic validator that ensures an attr value is in a
> list before assigning, and if not, assigns a default.  It works for
> self.attrib='value', and Dummy.new.attrib='value', but not
> @attrib='value' within the class.

The connection between a method foo=(val) and some instance variable
@foo is essentially coincidental.

When you use a class method such as attr_accessors it dynamically
creates code that defines accessor methods, and in those methods uses an
instance variable with a matching name.

But there's no reason the instance variable in those accessors methods
could not called something else. It just makes a certain sense to use an
obvious naming convention; it's so much easier to track what your code
is doing.  And it creates (for better or worse) the illusion of public
"properties" (as, for example, what Java has).

There's nothing to stop other methods from manipulating those instance
variables; they have no intrinsic connection to any particular methods,
no matter what they are named.

# Runs, but pedantic
def foo=(x); @bar=x;end

def baz; @bar; end

def foo; 47; end

When you do  @attrib = 47 you are working directly with the instance
variable, not with a method that might just happen to have a matching
name.




--
James B.

www.happycamperstudios.com   - Wicked Cool Coding
www.jamesbritt.com           - Playing with Better Toys
www.ruby-doc.org             - Ruby Help & Documentation
www.rubystuff.com            - The Ruby Store for Ruby Stuff
Rick D. (Guest)
on 2009-02-11 17:42
(Received via mailing list)
On Tue, Feb 10, 2009 at 11:01 PM, Mike C.
<removed_email_address@domain.invalid>wrote:

> I want to write a generic validator that ensures an attr value is in a
> list before assigning, and if not, assigns a default.  It works for
> self.attrib='value', and Dummy.new.attrib='value', but not
> @attrib='value' within the class.  Any way to do that?


No, not in general.  @attrib = value involves a primitive assignment
operator which isn't a method invocation and therefore can't be
overriden.

The best you can do is to impose a discipline and avoid direct iv
assignment
to the varlable(s) you want to validate within the methods of that
class.

Here's the code:


If I may have to temerity to offer some critique:


> module Validator
>
>  def validate_is_member_of( attrib, list, default )
>    original_method = instance_method( "#{attrib}=".to_sym )


>    define_method( "#{attrib}=".to_sym ) do |val|
>      instance_variable_set( "@#{attrib}", (list.include?( val ) ? val :
> default ))
>    end
>  end



Not sure why you are doing with the original_method variable since it's
never used.  In the code below the getter method for the type attribute
generated by attr_accessor :name :type is just discarded.


>    @name = name
>    @type = type
>  end
> end
>
> good = Dummy.new('good', :veggie )      # => ... @type=:veggie
> good.type = :ice_cream                              # => ...
> @type=:fruit.  i know i overwrote a valid value. it's ok.


@type here is not an instance varlable of an instance of Dummy, it's an
instance variable of the top-level object, so this line is moot.

>
> bad = Dummy.new('bad', :chocolate )     # => ... @type=:chocolate; I
> want :fruit here.


Now, if I were to approach this I might change the dsl a bit and have
the
class method take on the job of attr_accessor and generate the getter
and
setter methods, For clarity I'd change the name validate_is_a_member_of.
Here's another swing at this:

module Validator

 def validated_attr( attrib, list, default=nil)
   attr_reader attrib
   define_method( "#{attrib}=".to_sym ) do |val|
     instance_variable_set( "@#{attrib}", (list.include?( val ) ? val
:default ))
   end
 end

end

class Dummy
 extend Validator

 attr_accessor :name
 validated_attr :type, [ :fruit, :veggie, :dairy ], :fruit

 def initialize( name, type )
   @name = name
   # here is an example of the discipline I mentioned, since initialize
is
an instance method,
   # it should use the setter method.
   self.type = type
 end
end

good = Dummy.new('good', :veggie )     # => #<Dummy:0x23eec
@name="good",
@type=:veggie>
good.type                              # => :veggie
good.type = :ice_cream
good.type                              # => :fruit
bad = Dummy.new('bad', :chocolate )    # => #<Dummy:0x23690 @name="bad",
@type=:fruit>
"I want :fruit here:"                  # => "I want :fruit here:"
bad.type                               # => :fruit
"No chocolate fo you kid!"             # => "No chocolate fo you kid!"


Note that this still doesn't prevent someone from sending
:instance_variable_set and bypassing this, using #send or #__send__ to
get
around the privacy..

Here's a slightly more complicated version which closes that hole, but
IMHO
this is really going a bridge too far.

module Validator

  module ClassMethods
    def validated_setters
      @validated_setters ||= {}
    end

    def validated_attr( attrib, list, default=nil)
      attr_reader attrib
      module_eval( "def #{attrib}=val;@#{attrib} =
#{list.inspect}.include?(val) ? val : #{default.inspect};end")
      self.validated_setters["@#{attrib}".to_sym] = :"#{attrib}="
    end
  end

  def self.included(other_mod)
    other_mod.extend ClassMethods
  end

  def send(symbol, *args, &block)
    if symbol == :instance_variable_set &&  setter =
self.class.validated_setters[args.first.to_sym]
      send(setter, args[1], &block)
    else
      super
    end
  end

  alias :__send__ :send
end

class Dummy
 include Validator

 attr_accessor :name
 validated_attr :type, [ :fruit, :veggie, :dairy ], :fruit

 def initialize( name, type )
   @name = name
   # here is an example of the discipline I mentioned
   self.type = type
 end
end

good = Dummy.new('good', :veggie )     # => #<Dummy:0x21714
@type=:veggie,
@name="good">
good.type                              # => :veggie
good.type = :ice_cream
good.type                              # => :fruit
bad = Dummy.new('bad', :chocolate )    # => #<Dummy:0x20f44
@type=:fruit,
@name="bad">
"I want :fruit here:"                  # => "I want :fruit here:"
bad.type                               # => :fruit
"No chocolate fo you kid!"             # => "No chocolate fo you kid!"
good.send(:instance_variable_set, :@type, :dairy)
good.type                              # => :dairy
good.send(:instance_variable_set, :@type, :arsenic)
good.type                              # => :fruit
good.__send__(:instance_variable_set, :@type, :arsenic)
good.type                              # => :fruit

--
Rick DeNatale

Blog: http://talklikeaduck.denhaven2.com/
Twitter: http://twitter.com/RickDeNatale
This topic is locked and can not be replied to.