hoodwink.d enhanced
RSS
2.0
XHTML
1.0

RedHanded

Methods That Self-Destruct #

by why in inspect

Expiring a method. And let’s keep meta out of this.

 class Trial
    def run_me
       def self.run_me; raise Exception, "NO MORE." end
       puts "Your trial period has ended." 
    end
 end

 t = Trial.new
 t.run_me
 #=> Your trial period has ended.
 t.run_me
 #=> (trial):3:in `run_me': NO MORE. (Exception)

The run_me overwrites itself. But notice it uses def self. This overwrites the method in the object, not in the class. So you can create more Trial objects which haven’t self-destructed yet.

Can also be used as a memoization technique. But, rather than caching the data, you just ensure that the code runs only once.

 class Hit
   def initialize(ip)
     @ip = ip
   end
   def country
     def self.country; @country end
     @country = `geoiplookup #{@ip}`.chomp.gsub(/^GeoIP Country Edition: /,"")
   end
 end

I should correct one thing. I’m not actually overwriting the method. Just capping it off with a new singleton method.

For more on singleton methods: an intro and some old thoughts on duck-typed singletons.

Oy: Don’t miss MenTaL’s method-rewriting state machine. Sixth comment down, that big one.

said on 11 Jul 2006 at 10:41

Yeah—this technique is simple, but really underappreciated.

However, if you’re using it to optimize @country ||= ..., it’s worth noting that the following technique creates a considerably faster accessor:

def country
  class << self ; attr_reader :country end
  @country = `geoiplookup #{@ip}`.
    chomp.gsub(/^GeoIP Country Edition: /,"")
end

A benchmark of 5,000,000 reads comes out like:

technique time (seconds)
@variable ||= 13.600000
def self.accessor 10.740000
class << self ; attr_reader 7.380000

As you can see, attr_reader wins by quite a bit.

said on 11 Jul 2006 at 11:16

class Fixnum def + (a) case rand 4 when 0 def + (b); self+b end when 1 def + (b); self*b end when 2 def + (b); self-b end when 3 def + (b); self/b end end end end

said on 11 Jul 2006 at 11:18

class Fixnum
  def + (a)
    case rand 4
      when 0
        def + (b); self+b end
      when 1
        def + (b); self*b end
      when 2
        def + (b); self-b end
      when 3
        def + (b); self/b end
    end
  end
end
said on 11 Jul 2006 at 11:30

Hmm… is this better than the traditional form of memoization? Or does it have adverse effects by comparison?

Also, I’m still looking for a way to task dependencies with parameters just by calling them like methods at the beginning of the task def—I wonder if this would work?

said on 11 Jul 2006 at 11:38

trans: it can be bad with inheritance—e.g. if somebody derives from Hit and overrides Hit#country, you will clobber that. Otherwise, singleton-methods-for-memoization is very win.

said on 11 Jul 2006 at 11:48

It’s worth nothing that self-replacing methods aren’t only for destruction, memoization, and messing with people who are fond of Fixnum#+ (I think that is most of us)—you can use them to maintain state. For example, here’s a state machine which recognizes a sequence like a(bc)*(a|c):


class Recognizer
  def initialize ; _start end

  def reset ; _start end

private
  def _start
    def self.match( k )
      case k
      when :a: _S0
      else _fail
      end
    end
    def self.finish
      _fail
    end
  end

  def _S0
    def self.match( k )
      case k
      when :b: _S1
      else _S2 ; match( k )
      end
    end
  end

  def _S1
    def self.match( k )
      case k
      when :c: _S0
      else _fail
      end
    end
  end

  def _S2
    def self.match( k )
      case k
      when :a: _end
      when :c: _end
      else _fail
      end
    end
  end

  def _end
    def self.match( k ) ; _fail ; end
    def self.finish ; end
  end

  def _fail
    def self.match( k ) ; _fail ; end
    def self.finish ; _fail ; end
    raise "match failed" 
  end
end

r = Recognizer.new
# will match
[ :a, :b, :c, :b, :c, :c ].each do |k|
  r.match k
end
r.finish

r.reset
# will fail
[ :a, :b, :b, :c, :a ].each do |k|
  r.match k
end
r.finish

This is an objectless embodiment of the State pattern, in much the same way that Ruby iterators are an objectless embodiment of the Visitor pattern.

said on 11 Jul 2006 at 11:49

MenTaLguY: Thanks for the benchmark. That’s really nice to know.

said on 11 Jul 2006 at 12:28

Since when have we been able to have def...end directly inside def...end, without a class definition nested between? I’ve not attempted this for a while but when I last tried it, I got a error. I concluded there was a reason for this by design, and left it there. Now, even with 1.8.2 I can. So, do we need define_method any more?

MenTaLguY: object-less State pattern: Nice!

I feel pulled two ways: this is powerful and the mechanism is clear, but it is self-modifying code, which one is taught to avoid. Or do I need to unlearn that? After all, goto is OK in the right circumstances…

said on 11 Jul 2006 at 12:40

hgs: you need to unlearn all that. Otherwise you’ll never do any metaprogramming. That rule against self-modifying code was for a static world. We now are boldly entering a new dynamic world and _why is leading the way! Onward to the glorious, dynamic future!

...OK, just don’t get too carried away with it. Use judiciously. Proceed with caution but tread with confidence. Remember: self-modifying code can smell fear – never let it know you’re afraid and you’ll be fine. A bit like lion taming in that regard.

said on 11 Jul 2006 at 12:54

Yes, good point about parallels with metaprogramming. And fear is the mind-killer, of course :-)

said on 11 Jul 2006 at 13:33

You’re welcome! However, I got curious how memoized reads compare to the one-time setup overhead. This is what I found (times are normalized):

technique setup  read    setup
read
||= 1.16 1.00 1.16
def self.accessor 5.24 0.790 6.63
class << self ; attr_reader 6.44 0.543 11.9

This means that it takes a few calls to “break even”, and a few more before one of the “faster” techniques finally wins:

calls ||=   def self attr_reader
1 1.16 5.24 6.44
2 2.16 6.03 6.98
3 3.16 6.82 7.53
4 4.16 7.61 8.07
5 5.16 8.40 8.61
6 6.16 9.19 9.16
7 7.16 9.98 9.70
8 8.16 10.8 10.2
9 9.16 11.6 10.8
10 10.2 12.4 11.3
11 11.2 13.1 11.9
12 12.2 14.0 12.4
13 13.2 14.7 13.0
14 14.2 15.5 13.5

I think there’s probably a lesson here…

said on 11 Jul 2006 at 13:39

Do you really need the self. here? Wouldn’t

def runme def runme; end end

Work just the same?

said on 11 Jul 2006 at 13:44

hgs: you’ve always (as far as I know) been able to do a def obj.meth outside a class or module, just never a regular def. The rationale: the receiver of the method definition is explicit when you’re defining a singleton method on an object, but if you’re outside a class or module and say def meth, where does it go?

self.class isn’t always a desirable answer for that, nor is “the class/module on which the enclosing method was defined”...

said on 11 Jul 2006 at 13:46

.: it’s not allowed syntax. If it were, I’m not sure people could agree on what it should mean.

said on 11 Jul 2006 at 13:52

MenTaLguY: Welp, I guess the lesson is:

 def country
   if @past == 12
     def self.country
       @past += 1
       @country
     end
   end
   @country ||= (@past = 0) && `geoip...`
   @past += 1
   @country
 end
said on 11 Jul 2006 at 13:53

pHiL: I think the thing to ask is always “does doing it this way make the code clearer?”

said on 11 Jul 2006 at 14:00

.: it’s not allowed syntax. If it were, I’m not sure people could agree on what it should mean.

His syntax works for me without warning or error on Ruby 1.8.4.


class Doggie
    def praise
        def praise; puts "Bad boy"; end
        puts "Good boy" 
    end
end

h = Doggie.new
3.times do h.praise end

r = Doggie.new
3.times do r.praise end

With one difference: def praise overwrites the method. That is, the output is one “Good boy” and five “Bad boy” instead of two “Good boy” and four “Bad boy,” which is what def self.praise does.

Fun.

More proof cats are for the wins and dogs are not.

said on 11 Jul 2006 at 14:10

why: Dude, no, it’s got to be something like this:


def country
  start = Time.now
  deffed = Thread.new {
    def self.country ; @country end
    self.country 
  }.value
  deffed_time = Time.now - start
  start = Time.now
  regular = Thread.new {
    @country ||= `geoip...`
  }.value
  regular_time = Time.now - start
  [ [ regular, regular_time ],
    [ deffed, deffed_time ] ].sort \
  do |(r0, t0), (r1, t1)|
    t0 <=> t1
  end.first.first
end
said on 11 Jul 2006 at 14:23

shadytrees: Wow, okay, see—I am very good at coming up with rationalizations for things which are totally wrong.

So, playing with it, the answer to where the method goes appears to be “the lexically innermost enclosing class/module block at the site where the method was defined, or Object if there is no such enclosing block”.

That means you have to be careful with def inside of a def obj.meth versus a def inside a def in class << obj. The former will put the method in the lexically enclosing class, not the singleton class of obj.

said on 11 Jul 2006 at 14:25

Nice timing, I just wrote up an article on the same thing, but alas with much less brevity and wit :(

said on 11 Jul 2006 at 15:25

attr_accessor and friends are faster than real methods because they throw out the scope:

$ parse_tree_show
class X
attr_reader :x
def y; @y; end
end
[[:class,
  :X,
  :Object,
  [:defn, :x, [:ivar, :@x]],
  [:defn, :y, [:scope, [:block, [:args], [:ivar, :@y]]]]]]
said on 11 Jul 2006 at 18:01

How does one look up old ruby blog articles on whytheluckystiff as per some old thoughts…?

said on 12 Jul 2006 at 07:39

nonni: Googling with the expression site:whytheluckystiff.net along with your other keywords will probably help until a better answer appears.

11 Jul 2010 at 21:29

* do fancy stuff in your comment.

PREVIEW PANE