On 8/6/07, Trans <transfire / gmail.com> wrote:
> This is quite interesting.

Thanks. I'd like it to be more than interesting; I want it to be
useful! Hopefully I can make some improvements.

> I'm not sure how I feel about the use of declarative style.
> I'm not a big fan of public, private, protected to begin
> with b/c of this.

I can understand if you have reservations about the style. Personally,
I'm used to it and I find it easy to read.

One notable difference between the public, private, protected notation
and these decorators is that these must appear immediately before each
method they should apply to. A decorator's effects don't stick around
beyond the very next method, so there's no danger of having it go
unnoticed further down the file.

> It also complicates the code dealing
> with method_added and wrapping methods... I wonder how
> robust it is. (This is another good example of where some
> built in AOP functionality could improve things.)

I worry about the interaction with existing method_added() or
singleton_method_added() hooks. I tested some straightforward examples
of those and found no problems. Also, this code is working with no
trouble in a rather large rails app in the company I work for.

The code would be simpler and safer if ruby treated metaclasses and
classes consistently by calling metaclass.method_added() instead of
(or in addition to) singleton_method_added(). (See my earlier mail
with subject "method_added hook and class methods" for more.)

Perhaps the following notation would be better:

class C
  decorate :memoized
  def tak(x, y, z)
    return z if x <= y
    tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y))
  end
end

It would obviate the need to redefine the decorator method itself and
thus simplify the implementation. Also, this notation is more explicit
about the mechanism.

> Though it's a bit less convenient, it might be better to
> just name the method:
>
>   class C
>     def tak(x, y, z)
>       return z if x <= y
>       tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y))
>     end
>     memoized :tak
>   end
>
> Unfortunately, not as nice, but the underlying code would
> certainly get simplified.

Yes, the implementation would be pretty easy. (There's even a similar
example, called "once", in the pickaxe book.) However, putting the
decorator at the bottom makes it easy to miss, especially if the
method body is long.

> Dreaming a little. I wonder, if there were a callback for when
> a class/module closes, then maybe you do do it lazily?

Yeah, when I started thinking about how to do this I looked for such a
callback but didn't find one.

> Also, I
> wonder if this corresponds to Matz' idea of ":"-notation he
> used for pre and post. So,
>
>   class C
>     def tak:memoized(x, y, z)
>       return z if x <= y
>       tak(tak(x - 1, y, z), tak(y - 1, z, x), tak(z - 1, x, y))
>     end
>   end

That's very interesting! I didn't know about that notation before. I
just read about pre, post, and wrap methods, which seem similar but
less useful. They are invoked at the method call, rather than the
method definition, so they have less chance to affect the method's
interface.

Now, if you could define arbitrary methods to be used with the
":"-notation, like def tak:memoized(x, y, z) in your example above,
that would be really useful.

> Oh, one last thing. Could you give some other examples?

Sure. The one I find most useful is tracing:

class Module
  TRACE_LEVEL = [0]

  decorator
  def traced(name, meth)
    lambda do |*args|
      s = '. ' * TRACE_LEVEL[0]
      puts s + "calling #{name}(#{args.map{|a|a.inspect}.join(',')})"
      TRACE_LEVEL[0] += 1
      r = begin
        begin
          meth.bind(self).call(*args)
        ensure
          TRACE_LEVEL[0] -= 1
        end
      rescue => ex
        puts(s + "! #{ex.class}: " + ex)
        raise ex
      end
      puts(s + '=> ' + r.inspect)
      return r
    end
  end
end

Then you can turn tracing on (or off) for any function easily:

class Calc
  class << self
    traced
    def fact(n)
      return 1 if n < 2
      return n * fact(n - 1)
    end

    traced
    def bomb(n)
      raise 'boo' if n < 2
      return n * bomb(n - 1) if n < 5
      begin
        return n * bomb(n - 1)
      rescue
        return n * fact(n - 1)
      end
    end
  end
end

Calc.fact(5)
Calc.bomb(5)

I've also used decorators to do database object lookups automatically.
For example, assume that a (hypothetical) web framework will call
Profile.show("37") when the user makes a request.

class Module
  decorator
  def lookup(name, f)
    lambda do |id|
      f.bind(self).call(self.class.find(id.to_i))
    end
  end
end

class Profile
  lookup
  def show(profile)
    return profile.name + ' is a nice person.'
  end

  lookup
  def edit(profile)
  end
end

Type checking (if you like that sort of thing):

(This one requires a small change that I will post shortly.)

class Module
  decorator
  def checked(name, f, types)
    lambda do |*args|
      [args, types].transpose.each do |a, t|
        raise TypeError if !a.is_a?(t)
      end
      f.bind(self).call(*args)
    end
  end
end

class C
  checked Integer, String
  def warn(level, message)
    STDERR.puts '!'*level + message
  end
end

You can also do general pre- and postconditions.

Deprecation warnings:

(Adapted from http://wiki.python.org/moin/PythonDecoratorLibrary.)

class Module
  decorator
  def deprecated(name, f)
    lambda do |*args|
      STDERR.puts "Warning: function #{name} is deprecated."
      f.bind(self).call(*args)
    end
  end
end

kr