Robert Klemme 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
···
On 09.10.2010 08:25, John Sikora wrote:
On Fri, Oct 8, 2010 at 6:06 AM, John Sikora<john.sikora@xtera.com> >> wrote:
--
remember.guy do |as, often| as.you_can - without end
http://blog.rubybestpractices.com/