From: Robert Klemme Date: 2010-10-11T02:10:13+09:00 Subject: Re: setting local variables in a binding On 09.10.2010 08:25, John Sikora wrote: > Robert Klemme wrote: >> On Fri, Oct 8, 2010 at 6:06 AM, John Sikora >> wrote: >> >> So you have something like >> >> class Foo >> def self.each >> yield "whatever" >> self >> end >> end >> >> ? Did you also do >> >> class Foo >> def self.each >> yield "whatever" >> end >> >> extend Enumerable >> end >> >> ? That would give you all methods like #find_all etc. for free. > > Robert, > Note that I have kept the code and comments to a minimum here as I did > not want the post to be even longer. I know that you are quite > knowledgeable and that you should have little trouble following what I > am doing - although you might not necessarily understand why I chose to > do it this way: Fluent speaking of Ruby makes it somewhat easier but even for me the length of the post needs some digesting. So I won't come up with a full coverage of everything you wrote for now. I hope my remarks are useful nevertheless. > class Base > @class_all_enum_objects = Array.new > > class<< self > attr_accessor :class_all_enum_objects > end > > def initialize > self.class.class_all_enum_objects<< self > end > > def self.enum > EnumeratorEOC.new(self, :each) > end > > def self.each(&code_block) > self.class_all_enum_objects.each do |object| > yield object > end > end > end > > module EnumeratorModifierEOC This should rather be a EnumerableModifierEOC IMHO since #find is defined in Enumerable and this module contains all the enumerating methods which are based on #each (e.g. find_all, inject, map). > def find_all(*condition_strings,&code_block) > if block_given? > find_array = super > else > find_array = # process condition strings (syntactic sugar) here. > end > ArrayEOC.new(find_array) > end > > # code for other methods such as sort, min, max, all?, etc. here. > end > > class EnumeratorEOC< Enumerator # avoids modifying Enumerator. > include EnumeratorModifierEOC > > def initilize(object, method_sym) > super > end > end > > class ArrayEOC< Array # avoids modifying Array. > include EnumeratorModifierEOC > > def initialize(array = []) > super(array) > end > end Although no additional methods are shown (these are presented below if I am not mistaken) the purpose of EnumeratorEOC and ArrayEOC is mainly to augment standard types with additional functionality it seems. Correct? > To suit my purposes: 1) I want the class itself to keep track of all of > the class instances, as shown above, 2) if an attribute has more than > one value or is more complex than an Array, I prefer to define a class > for it (inherited from Base) and assign attributes, 3) I want syntactic > sugar for enumerables, 4) especially for sort, including default sort > parameters (min and max get defaults for free as a result of the > necessary<=> definition). 5) I want to be able to define new functions > and to be able to use syntactic sugar with them. See below. 6) I also > keep track of descendants of classes although this is only partly shown > below. There is a little more on this at the end if anyone is still > reading. > >> >>> and I have added syntactic sugar to some of the methods. So I can be >>> lazy (and quick) and write things like: >>> >>> Slot.enum.find_all(�:slot_num>= 8�, �:slot_num<= 22�, �:rate == >>> �ufec��).all?(�:errors == 0�) >> >> Is this equivalent to >> >> enum.find_all {|s| (8..22) === s.slot_num&& s.rate == 'ufec'}.all? >> {|s| e.errors == 0} > > Yes, you are correct (e.errors is a typo - should be s.errors), they are > equivalent. Sorry, that was a typo. Thanks for catching that. >> ? If yes, I do not really see the advantage of your approach. It's >> slower needs a similar amount of typing and will detect errors later >> (at execution time vs. at parse time). >>> >> If my assumption above is correct I do not understand where the "easy >> way out" is. >> > I think 'sort' is the main reason for my 'easy way out'. Plus the use of > additional functions. To illustrate, I have included in the code below > one such function, 'sum_total_eoc' which is just 'inject' set up for > addition, but with the ability to use syntactic sugar (of course). In the code above I do not see any usage of #sort so I don't really see the easy way out in the example above. Even if you would need the String argument eval solution for sorting it would not be necessary for the code above which uses #find_all and #all?. > You may or may not agree with my 'easy way out', but here goes: > > First, a short explanation: I have attribute and expression comparables > for each class. The comparables are compared in the order that they are > given in attr_accessor. There is also a class method called > set_comparables_order that allows the user to re-arrange the order if > desired (it also allows expressions, not just attributes). Child classes > inherit the comparables and their order from the parent class. Any new > attributes defined in attr_accessor in the child class are added to the > comparables up front. A problem of your design is that you change the classes idea of default ordering. What I mean is this: you need to modify a class to achieve a particular ordering. Now the standard behavior of sorting has changed and you can never tell what ordering you will get by only looking at a particular piece of code which only contains the call to #sort. IMHO it would be better to not allow this but instead create a mechanism which makes the ordering explicit. Here is one approach module EnumeratorModifierEOC # or Enumerable def sort_by_fields(*fields) sort_by {|x| fields.map {|f| x.send(f)}} end end This might not be the most efficient solution because of the repeated invocation of #map. Alternatively you can easily stick with #sort_by by doing enum.sort_by {|x| [x.field_1, x.field_2]} which is pretty short, readable and efficient. You could improve this by creating something like a Sorter: def Sorter(*fields) eval "lambda {|enum| enum.sort_by{|x| [#{fields.map {|f| "x.#{f}"}.join(', ')}]}}" end MAIN = Sorter(:foo, :bar) ... sorted = MAIN[enum] Now you actually have your eval but you use it to compile code which you can then use more often efficiently. :-) You can even make this more modular by removing the sorting and compiling only the field extraction: def FieldExtractor(*field) eval "lambda {|x| [#{fields.map {|f| "x.#{f}"}.join(', ')}]}" end MAIN = FieldExtractor(:foo, :bar) ... sorted = enum.sort_by &MAIN > OK, so what can I do with this? The following are some simplified > examples: > > class Slot< EnumerableObjectClass::Base > attr_accessor :slot_num, :rate, :error_info #<--Array of ErrorInfo > objects. > > def initialize(slot_num, rate, error_info) > super() > @slot_num, @rate, @error_info = slot_num, rate, error_info > end > end > > class ErrorInfo< EnumerableObjectClass::Base > attr_accessor :time_stamp, :num_errors > > def initialize(time_stamp, num_errors) > super() > @time_stamp, @num_errors = time_stamp, num_errors > end > end > > Slot.new(3, 'efec', [ErrorInfo.new('10-07-10', 500), > ErrorInfo.new('10-08-10', 1000)]) > Slot.new(1, 'ufec', [ErrorInfo.new('10-04-10', 1000), > ErrorInfo.new('10-05-10', 2000)]) > Slot.new(8, 'ufec', [ErrorInfo.new('10-07-10', 1500), > ErrorInfo.new('10-08-10', 1200)]) > Slot.new(4, 'efec', [ErrorInfo.new('10-07-10', 3500)]) > Slot.enum{}.find(':slot_num == 4').error_info<< > ErrorInfo.new('10-08-10', 2500) > > ------------- > # note - most of the examples below do not use variables. However, this > is not a limitation and a couple of examples are given that use > variables. > > # default sort is sorted by :slot_num, etc. - order of attr_accessor > p Slot.enum.sort.collect(:slot_num) > # => [[1], [3], [4], [8]] > > # sort by total errors. > p > Slot.enum.sort(':error_info.sum_total_eoc(:num_errors)').collect(:rate, > ':error_info.sum_total_eoc(:num_errors)') > # => [["efec", 1500], ["ufec", 2700], ["ufec", 3000], ["efec", 6000]] > > # sort by rate, total errors. > p Slot.enum.sort(:rate, > ':error_info.sum_total_eoc(:num_errors)').collect(:rate, > ':error_info.sum_total_eoc(:num_errors)') > # => [["efec", 1500], ["efec", 6000], ["ufec", 2700], ["ufec", 3000]] > > # sort by rate, total errors, with total errors sorted high to low by > ending with '___' triple underscore. > p Slot.enum.sort(:rate, > ':error_info.sum_total_eoc(:num_errors)___').collect(:rate, > ':error_info.sum_total_eoc(:num_errors)', :slot_num) > # => [["efec", 6000, 4], ["efec", 1500, 3], ["ufec", 3000, 1], ["ufec", > 2700,8]] > > I think that the above would be hard to do in a one-liner w/o syntactic > sugar, so this is my 'easy way out' (although I would not be surprised > if you came up with one Robert). I think this is pretty easy and it > saves me time. The tricky thing I did not consider yet is that you have nested access to fields and not just one level. In my world this would look like p Slot.enum.sort_by {|x| [x.rate, x.error_info.sum_total_eoc(:num_errors)___]}. map {|x| [x.:rate, x.error_info.sum_total_eoc(:num_errors), x.slot_num]} That's certainly not nice to squeeze on one line but your solution does not look that much shorter. > # total errors of all slots. > p Slot.enum.sum_total_eoc(':error_info.sum_total_eoc(:num_errors)') > # => 13200 > > # slots that have>= 3000 total errors. > p Slot.enum.find_all(':error_info.sum_total_eoc(:num_errors)>= > 3000').collect(:slot_num) > # => [[1], [4]] > > # use a variable for sort parameter. > var = ':slot_num' > p Slot.enum.sort("#{var}").collect("#{var}") > # => [[1], [3], [4], [8]] > > # use a variable w/o interpolation - must pass a binding with enum{} > (which is where this topic started by the way). It is carried along as > an attribute to ArrayEOC and/or EnumeratorEOC, similar to coparables. > var = ':slot_num' > p Slot.enum{}.sort(var).collect(var) > # => [[1], [3], [4], [8]] > > # slots that have>= 2500 errors in a single time period. > p Slot.enum.find_all(":error_info.any?(:num_errors>= > 2500)").collect(:slot_num) > # => Exceptions galore! my simple 'parser' breaks down. will have to use > the 'old fashioned way' of a code block. maybe I will try to get this to > work in the future. > > In case anyone is wondering, if data is input as an Array, it comes out > as an ArrayEOC the first time it is read. In the simplified code above, > I show a call to super in attr_accessor, but really for the reader part, > I add a line that converts an Array to an ArrayEOC. I do this so I can > use the ArrayEOC methods as mofified since I want to keep Array's > methods unmodified. > > Since I already use self.inherited, I am able to keep track of > descendants (a recursive jaunt through the child classes using inject to > build an array), so there is enum_only{} which enumerates only the given > class, and enum{} which enumerates the given class and all descendants. > I do this because I have a number of units of similar type which follow > the classic inheritance OO model (even though it is apparent from posts > that this has fallen out of favor). Oh well, it seems that I am behind > the times on a lot of things. > > Oh, and since Ruby is dynamic, I had to modify remove_method and > undefine_method to remove any comparables, if applicable (there is a > difference between the two however). And if there are subsequent calls > to attr_accessor, I have to add those methods to the class comparables > in the class and any descendant classes. I allow the user to reset the > comparables to those given in the class and its ancestors. I also tried > to make the code reflective, by making the comparables, child_classes, > descndants, etc. availble. Your need to redefine remove_method etc. is fallout of your design decision to change the default ordering. As I said, I believe there are better and more efficient designs. > Writing this code has certainly been fun and > educational! That's good! There are still some things that I didn't yet wrap my head around: why do you want to make classes keep track of all their instances? This essentially makes classes global variables - but less obvious. During the course of a program usually you create instances and let forget them again. But in your situation you will keep all instances around and there is no clear demarcation. If you want automated tracking of instances there are other options, e.g. class InstanceTracker def initialize @all = Hash.new {|h,k| h[k] = []} end def new(cl,*args, &b) @all[cl] << cl.new(*args, &b) end def clear @all.clear self end def clear_class(cl) @all.delete(cl) self end include Enumerable def each(&b) if b @all.each {|k,v| v.each(&b)} self else Enumerator.new(self, :each) end end def each_class(cl, &b) if b @all[cl].each(&b) self else Enumerator.new(self, :each_class, cl) end end def each_sub_classes(cl, &b) if b @all.each {|k,v| v.each(&b) if k <= cl} self else Enumerator.new(self, :each_sub_classes, cl) end end end it = InstanceTracker.new it.new(Foo, 1, 2) it.each_class(Foo).find_all {|f| f.size > 0} etc. Kind regards robert -- remember.guy do |as, often| as.you_can - without end http://blog.rubybestpractices.com/