From: Keith Rarick Date: 2007-08-08T05:45:01+09:00 Subject: Re: python-style decorators On 8/6/07, Trans 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