Some of my earlier questions may have hinted ever so slightly in the direction that I''m trying to implement "enumerations". By an enumeration here I mean a class that has a fixed number of immutable instances, which in turn have essentially only a name and a position. Requirements I''ve tried to meet are - Enumeration instances should only be loaded once from the database. - It should be easy to define enumerations; in particular it should not be necessary to define a model class in a separate file for each enumeration. - It should be possible to store each enumeration in its own table or all enumerations in a single table. The resulting syntax looks like this: BoilerPlate::Enumerations::define_enums( ''FirstEnum'', {:class_name => ''AnotherEnum, :table_name => ''second_enum'' } ) Further options are :position for ordering and :conditions which can be used to map records from the same table to different enumeration classes. Include a code snippet like the one above in config/environment.rb and that''s all there is to it. Apart from defining the requisite database tables, of course. Enumeration classes are only defined when they are accessed the first time. Instances are loaded and cached, the first time they are accessed through, say, FirstEnum.all. If the need should arise, the cache can be cleared with FirstEnum.reset. Autodefining works by wrapping around the Module#const_missing hook already used by Rails''s dependency mechanism. Below is the code. I appreciate your comments. Michael require ''active_support/dependencies'' module BoilerPlate # :nodoc: module Enumerations @@enumeration_specs = {} def self.define_enums(*enums) enums.each do |spec| if spec.kind_of?(Hash) class_name = spec[:class_name] raise ArgumentError, ''An enumeration specification must contain a :class_name'' if class_name.empty? class_name = class_name.to_sym else class_name = spec.to_sym spec = { :class_name => class_name } end @@enumeration_specs[class_name] = spec end end class EnumRecord < ActiveRecord::Base @@all = nil def self.reset @@all = nil end protected def after_initialize freeze end end def self.define_if_enumeration(const_id) spec = @@enumeration_specs[const_id] return nil unless spec order = spec[:order] || ''position'' class_def = <<-END class #{spec[:class_name]} < BoilerPlate::Enumerations::EnumRecord def self.all return @@all if @@all @@all = find(:all, :order => ''#{order}'', :conditions => #{spec[:conditions].inspect}) end end END eval(class_def, TOPLEVEL_BINDING) enum_class = const_get(spec[:class_name]) enum_class.table_name = spec[:table_name] if spec[:table_name] enum_class end end end class Module # :nodoc: alias_method :const_missing_without_enums, :const_missing def const_missing(const_id) const_missing_without_enums(const_id) rescue NameError => e enum_class = BoilerPlate::Enumerations::define_if_enumeration(const_id) raise unless enum_class enum_class end alias_method :const_missing_with_enums, :const_missing end -- Michael Schuerig Those people who smile a lot mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org Watch the eyes http://www.schuerig.de/michael/ --Ani DiFranco, Outta Me, Onto You
In article <200508200053.25029.michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org>, michael- q5aiKMLteq4b1SvskN2V4Q-XMD5yJDbdMReXY1tMh2IBg@public.gmane.org says...> > Some of my earlier questions may have hinted ever so slightly in the > direction that I''m trying to implement "enumerations". By an > enumeration here I mean a class that has a fixed number of immutable > instances, which in turn have essentially only a name and a position.This seems really, really useful, and I imagine could solve a lot of needs for multi-layer eager associations as well - I know most of my imagined uses for that involved such lookup tables.. As a Rails newbie, I''m having some trouble teasing out just how to use this from the source code... questions: - What should the actual enum table(s) contain? One integer column named "position", I guess, and the main column should be called, er, what? - Can I use these in belongs_to relationships as I''d expect? How do I include an enum in a model? - If so, wouldn''t it be more idiomatic to call the position column "id"? In my mental model of enums, it''d go something like this: class IceCreamCone << ActiveRecord::Base belongs_to :flavor # how I wish this were called refers_to end BoilerPlate::Enumerations::define_enums( ''Flavor'' ) create table ice_cream_cones ( id serial not null, ... flavor_id int); create table flavors ( id serial not null, name text); But that doesn''t seem to be how this is working. Help? -- Jay Levitt | Wellesley, MA | I feel calm. I feel ready. I can only Faster: jay at jay dot fm | conclude that''s because I don''t have a http://www.jay.fm | full grasp of the situation. - Mark Adler
On Saturday 20 August 2005 01:33, Jay Levitt wrote:> In article <200508200053.25029.michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org>, michael- > q5aiKMLteq4b1SvskN2V4Q-XMD5yJDbdMReXY1tMh2IBg@public.gmane.org says... > > > Some of my earlier questions may have hinted ever so slightly in > > the direction that I''m trying to implement "enumerations". By an > > enumeration here I mean a class that has a fixed number of > > immutable instances, which in turn have essentially only a name and > > a position. > > This seems really, really useful, and I imagine could solve a lot of > needs for multi-layer eager associations as well - I know most of my > imagined uses for that involved such lookup tables..I''m not sure if I understand you correctly here. I don''t see how you could avoid eager associations. ActiveRecord is not an O/R-Mapper like Hibernate, say, that caches one unique canonical instance of each accessed object. In Rails caching of objects used as enumerations, is mainly useful to avoid repeated queries to populate lists or select elements with the same objects again and again.> As a Rails newbie, I''m having some trouble teasing out just how to > use this from the source code... questions: > > - What should the actual enum table(s) contain? One integer column > named "position", I guess, and the main column should be called, er, > what?That''s actually up to you. I use a "name" column apart from "position" and "id".> - Can I use these in belongs_to relationships as I''d expect? How do > I include an enum in a model?Just as with any other class. What this snippet does BoilerPlate::Enumerations::define_enums( ''FirstEnum'' ) is define the class FirstEnum derived from ActiveRecord::Base. You could as well have written it manually. But the point is exactly to avoid the latter.> - If so, wouldn''t it be more idiomatic to call the position column > "id"?No, "position" is not the primary key, it is used for ordering. It defines the order of instances returned by, e.g., FirstEnum.all. You need an "id" column in addition.> In my mental model of enums, it''d go something like this: > > class IceCreamCone << ActiveRecord::Base^^ only <> belongs_to :flavor # how I wish this were called refers_to > end > > BoilerPlate::Enumerations::define_enums( > ''Flavor'' > ) > > create table ice_cream_cones ( > id serial not null, > ... > flavor_id int); > > create table flavors ( > id serial not null,position int,> name text); > > But that doesn''t seem to be how this is working. Help?It is intended to work exactly like that. I call define_enums in a file required (indirectly) from config/environment.rb. Otherwise I''m doing exactly what you describe. Have you tried it and encountered any problems? Michael -- Michael Schuerig This is not a false alarm mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org This is not a test http://www.schuerig.de/michael/ --Rush, Red Tide
why not just create an acts_as_enum :field_name acts_as ''helper''? (i know thats not what tehre called) you can use acts_as_list to do ordering (positions) already. On 8/19/05, Jay Levitt <jay-news-WxwZQdyI2t0@public.gmane.org> wrote:> In article <200508200053.25029.michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org>, michael- > q5aiKMLteq4b1SvskN2V4Q-XMD5yJDbdMReXY1tMh2IBg@public.gmane.org says... > > > > Some of my earlier questions may have hinted ever so slightly in the > > direction that I''m trying to implement "enumerations". By an > > enumeration here I mean a class that has a fixed number of immutable > > instances, which in turn have essentially only a name and a position. > > This seems really, really useful, and I imagine could solve a lot of > needs for multi-layer eager associations as well - I know most of my > imagined uses for that involved such lookup tables.. > > As a Rails newbie, I''m having some trouble teasing out just how to use > this from the source code... questions: > > - What should the actual enum table(s) contain? One integer column > named "position", I guess, and the main column should be called, er, > what? > > - Can I use these in belongs_to relationships as I''d expect? How do I > include an enum in a model? > > - If so, wouldn''t it be more idiomatic to call the position column "id"? > > In my mental model of enums, it''d go something like this: > > class IceCreamCone << ActiveRecord::Base > belongs_to :flavor # how I wish this were called refers_to > end > > BoilerPlate::Enumerations::define_enums( > ''Flavor'' > ) > > create table ice_cream_cones ( > id serial not null, > ... > flavor_id int); > > create table flavors ( > id serial not null, > name text); > > But that doesn''t seem to be how this is working. Help? > > -- > Jay Levitt | > Wellesley, MA | I feel calm. I feel ready. I can only > Faster: jay at jay dot fm | conclude that''s because I don''t have a > http://www.jay.fm | full grasp of the situation. - Mark Adler > > _______________________________________________ > Rails mailing list > Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org > http://lists.rubyonrails.org/mailman/listinfo/rails >-- Zachery Hostens <zacheryph-Re5JQEeQqe8AvxtiuMwx3w@public.gmane.org>
On Saturday 20 August 2005 04:49, Zachery Hostens wrote:> why not just create an acts_as_enum :field_name acts_as ''helper''? > (i know thats not what tehre called)What''s that supposed to do? One of the purposes of enumerations, as suggested in my original message, is to avoid having individual files for a number of classes which are apart from their name all the same. Michael -- Michael Schuerig All good people read good books mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org Now your conscience is clear http://www.schuerig.de/michael/ --Tanita Tikaram, Twist In My Sobriety
Hi Michael, that''s some pretty tricky stuff. Nice job - now I can get rid of all those pesky lookup models I hand-coded last week. They were pretty smelly even with extending from an abstract base class... A couple of comments on your implementation: Firstly, I noticed that the @@all cache is a bit too far-reaching. Given your example of:> BoilerPlate::Enumerations::define_enums( > ''FirstEnum'', > {:class_name => ''AnotherEnum, :table_name => ''second_enum'' } > )In my tests AnotherEnum.all and FirstEnum.all both return the same results despite having different table contents. By changing each occurrence of @@all to just @all everything worked as I would expect (I also removed the initial assignment of @@all = nil too). Second, consider an enumeration called "Status" with values of "active", "inactive", "retired" etc. Assuming I want to actually do something depending on the value of the status I think it''s pretty ugly (and brittle) to code it up like this: case model.status.name when "active" do_something when "inactive" do_something_else end If you mistype "active" or "inactive" it just silently fails when you really want it to blow up spectacularly (and early). So I came up with this: case model.status when Status[:active] do_something when Status[:inactive] do_something_else end It''s pretty simple to code up a def self.[](arg) method to look for the relevant enumerated value in the @all cache, keying on name. And of course, the [](arg) method has the chance to raise an easy-to-understand ArgumentError if the name isn''t in the enumeration. Oh but that''s not all! The [](arg) method could lookup by name if the arg is a Symbol and by id if the arg is a Fixnum. That way you can do fun stuff like this in your rhtml files: <%= Status[model.status_id].description %> No extra hits to the database to grab that pesky "description" field from the enumeration as you print this line for 50 different model instances. Okay, I know eager associations help here but I already wanted to grab by ID because of the last little enhancement: Finally, it sure would be cool to be able to do the following in my models: validates_inclusion_of :status_id, :in => Status, :message => "Not a Valid Status" That just requires a simple def self.include?(arg) method that returns true if the arg matches one of the ids in the @all cache. So that''s it. If you want I can send you my changes but to be honest they aren''t rocket science and I''m sure you''ll have no trouble implementing any of the ideas you think have merit. Thanks, Trevor
Hi (again) Michael, I started worrying about what I said here: On 20-Aug-05, at 10:36 PM, Trevor Squires wrote:> In my tests AnotherEnum.all and FirstEnum.all both return the same > results despite having different table contents. By changing each > occurrence of @@all to just @all everything worked as I would expect > (I also removed the initial assignment of @@all = nil too). >I''m too much of a ruby newbie to say for certain whether changing @@all to @all is such a good idea. I think I came up with a better solution, which is to move the @@all = nil line and the reset method out of the definition for EnumRecord and put them into the autogenerated class definition. Again, after that change it works as I would expect (each enum has it''s own cache in @@all) but at least now ''all'' looks more like a class variable to me... Regards, Trevor
On Sunday 21 August 2005 07:36, Trevor Squires wrote:> Hi Michael, > > that''s some pretty tricky stuff. Nice job - now I can get rid of all > those pesky lookup models I hand-coded last week. They were pretty > smelly even with extending from an abstract base class... > > A couple of comments on your implementation: > > Firstly, I noticed that the @@all cache is a bit too far-reaching. > > Given your example of: > > BoilerPlate::Enumerations::define_enums( > > ''FirstEnum'', > > {:class_name => ''AnotherEnum, :table_name => ''second_enum'' } > > ) > > In my tests AnotherEnum.all and FirstEnum.all both return the same > results despite having different table contents. By changing each > occurrence of @@all to just @all everything worked as I would expect > (I also removed the initial assignment of @@all = nil too).Yes, sigh, I tend to get tangled in the difference of class variables and instance variables of the class. It should be @all everywhere.> So I came up with this: > > case model.status > when Status[:active] > do_something > when Status[:inactive] > do_something_else > end > > It''s pretty simple to code up a def self.[](arg) method to look for > the relevant enumerated value in the @all cache, keying on name. And > of course, the [](arg) method has the chance to raise an > easy-to-understand ArgumentError if the name isn''t in the > enumeration.Yes, although I have an aversion to switch statements of this kind. Business logic has a tendency to go that way. I don''t know how to improve on this by adding some facility to enums. Concepts that crossed my mind are finite state machine, double dispatch, singleton class -- some ways to attach behavior to states, but only specific to a given context. (No, I don''t expect the previous sentence to make much sense to others.)> Oh but that''s not all! The [](arg) method could lookup by name if > the arg is a Symbol and by id if the arg is a Fixnum. That way you > can do fun stuff like this in your rhtml files: > > <%= Status[model.status_id].description %>Yes.> Finally, it sure would be cool to be able to do the following in my > models: > > validates_inclusion_of :status_id, :in => Status, :message => "Not a > Valid Status" > > That just requires a simple def self.include?(arg) method that > returns true if the arg matches one of the ids in the @all cache. > > So that''s it. If you want I can send you my changes but to be honest > they aren''t rocket science and I''m sure you''ll have no trouble > implementing any of the ideas you think have merit.Done. Thanks for your comments. I''ll be writing a few unit tests to avoid further embarassment. I''m currently in the process of preparing a new release of BoilerPlate[*] where Enumerations are included. Michael [*] http://www.schuerig.de/michael/boilerplate/ -- Michael Schuerig They tell you that the darkness mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org Is a blessing in disguise http://www.schuerig.de/michael/ --Janis Ian, From Me To You
> > So I came up with this: > > > > case model.status > > when Status[:active] > > do_something > > when Status[:inactive] > > do_something_else > > end > > Yes, although I have an aversion to switch statements of this kind. > Business logic has a tendency to go that way. I don''t know how to > improve on this by adding some facility to enums. Concepts that crossed > my mind are finite state machine, double dispatch, singleton class -- > some ways to attach behavior to states, but only specific to a given > context. (No, I don''t expect the previous sentence to make much sense > to others.) >The solution, is polymorhism :) model.status.do_polymorhpic_something In my Java experience objects that start off as Enums frequently end up with real behavior, often polymorphicly. I realize this makes it a touch counter to what you''re trying to acheive with Enumerations, but when the code calls for polymorphism, I think it should be used.
On Sunday 21 August 2005 15:08, David Corbin wrote:> > > So I came up with this: > > > > > > case model.status > > > when Status[:active] > > > do_something > > > when Status[:inactive] > > > do_something_else > > > end > > > > Yes, although I have an aversion to switch statements of this kind. > > Business logic has a tendency to go that way. I don''t know how to > > improve on this by adding some facility to enums. Concepts that > > crossed my mind are finite state machine, double dispatch, > > singleton class -- some ways to attach behavior to states, but only > > specific to a given context. (No, I don''t expect the previous > > sentence to make much sense to others.) > > The solution, is polymorhism :) > > model.status.do_polymorhpic_somethingYes, of course. But in Ruby there are ways to achieve polymorphism that are not available in Java. That''s what I alluded to with singleton class above. It''s entirely possible to make instances from the same enumeration behave differently by defining methods on a per instance basis: class <<Status[:active] def do_polymorphic_something ... end end Michael -- Michael Schuerig Failures to use one''s frontal lobes mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org can result in the loss of them. http://www.schuerig.de/michael/ --William H. Calvin
I am too fresh in Ruby to be suggesting something really, but nevertheless here we go :) I personally is not vary attracted to adding methods to the enum itself that is supposed to do something based on a given enum. That enum is only the test for if something is to be done or not. The actual method is most often something related to the object doing the switch. So in my book you''d use either a switch (that''s what they are designed to do :), or something like this (My ruby sucks, I am a n00b, but I am sure some of you veterans know how to code it): eval(:do_something_ + Status[:action]) ... although it sure could need a wrapper to check for exceptions... Personally I''d rather go for a switch... 0.02$, ya know... ------------------------------------------------------------------------ /*Ronny Hanssen*/ Michael Schuerig wrote:> On Sunday 21 August 2005 15:08, David Corbin wrote: > >>>>So I came up with this: >>>> >>>>case model.status >>>>when Status[:active] >>>> do_something >>>>when Status[:inactive] >>>> do_something_else >>>>end >>> >>>Yes, although I have an aversion to switch statements of this kind. >>>Business logic has a tendency to go that way. I don''t know how to >>>improve on this by adding some facility to enums. Concepts that >>>crossed my mind are finite state machine, double dispatch, >>>singleton class -- some ways to attach behavior to states, but only >>>specific to a given context. (No, I don''t expect the previous >>>sentence to make much sense to others.) >> >>The solution, is polymorhism :) >> >>model.status.do_polymorhpic_something > > > Yes, of course. But in Ruby there are ways to achieve polymorphism that > are not available in Java. That''s what I alluded to with singleton > class above. It''s entirely possible to make instances from the same > enumeration behave differently by defining methods on a per instance > basis: > > class <<Status[:active] > def do_polymorphic_something > ... > end > end > > Michael >
Doh... Forgot to mention that "action" would be an enum in Status, and that i.e. do_something_action would be a predefined method in the object doing the query... ------------------------------------------------------------------------ /*Ronny Hanssen*/ Ronny Hanssen wrote:> I am too fresh in Ruby to be suggesting something really, but > nevertheless here we go :) > > I personally is not vary attracted to adding methods to the enum itself > that is supposed to do something based on a given enum. That enum is > only the test for if something is to be done or not. The actual method > is most often something related to the object doing the switch. So in my > book you''d use either a switch (that''s what they are designed to do :), > or something like this (My ruby sucks, I am a n00b, but I am sure some > of you veterans know how to code it): > > eval(:do_something_ + Status[:action]) > > ... although it sure could need a wrapper to check for exceptions... > > Personally I''d rather go for a switch... > > 0.02$, ya know... > > ------------------------------------------------------------------------ > /*Ronny Hanssen*/ > > Michael Schuerig wrote: > >>On Sunday 21 August 2005 15:08, David Corbin wrote: >> >> >>>>>So I came up with this: >>>>> >>>>>case model.status >>>>>when Status[:active] >>>>> do_something >>>>>when Status[:inactive] >>>>> do_something_else >>>>>end >>>> >>>>Yes, although I have an aversion to switch statements of this kind. >>>>Business logic has a tendency to go that way. I don''t know how to >>>>improve on this by adding some facility to enums. Concepts that >>>>crossed my mind are finite state machine, double dispatch, >>>>singleton class -- some ways to attach behavior to states, but only >>>>specific to a given context. (No, I don''t expect the previous >>>>sentence to make much sense to others.) >>> >>>The solution, is polymorhism :) >>> >>>model.status.do_polymorhpic_something >> >> >>Yes, of course. But in Ruby there are ways to achieve polymorphism that >>are not available in Java. That''s what I alluded to with singleton >>class above. It''s entirely possible to make instances from the same >>enumeration behave differently by defining methods on a per instance >>basis: >> >>class <<Status[:active] >> def do_polymorphic_something >> ... >> end >>end >> >>Michael >> > > _______________________________________________ > Rails mailing list > Rails-1W37MKcQCpIf0INCOvqR/iCwEArCW2h5@public.gmane.org > http://lists.rubyonrails.org/mailman/listinfo/rails
On Monday 22 August 2005 00:47, Ronny Hanssen wrote:> I personally is not vary attracted to adding methods to the enum > itself that is supposed to do something based on a given enum. That > enum is only the test for if something is to be done or not. The > actual method is most often something related to the object doing the > switch.I agree on the latter, but not the former. I''m sure there is an elegant way to polymorphically, i.e. based on the enum instance, trigger an action that is dependent on the context. Here''s an example (maybe someone recognizes the movie I just watched). Say, you have enumerations for various medical qualities, such as BloodPh, with instances for high, medium, low. Now, as it happens, people with medium blood pH will die instantly when exposed to Andromeda, the others survive. With Enumerations as I introduced them, you could piggyback this information like this class <<BloodPh[:high] def survives_andromeda; true; end end class <<BloodPh[:medium] def survives_andromeda; false; end end class <<BloodPh[:low] def survives_andromeda; true; end end Let''s say we do something similar for other enums, then we could write this method class Patient def will_survive survives_common_cold && survives_andromeda && ... end def survives_andromeda blood_ph.survives_andromeda end ... end Well, this is surely not the best possible example, but I hope you see what I mean. Michael -- Michael Schuerig Face reality and stare it down mailto:michael-q5aiKMLteq4b1SvskN2V4Q@public.gmane.org --Jethro Tull, Silver River Turning http://www.schuerig.de/michael/
Sure. I see your point. I guess there are cases where the polymorphic pattern as suggested would apply. It would also be good to read the code too. I am a true member of the KISS society and following their mantra I guess the code would have a bit higher entry point in terms of understanding the code. But that''s a second priority, even though it would of course be nice if it was understood as easily as it was read :) Perfect world you know :) Ronny Michael Schuerig wrote:> On Monday 22 August 2005 00:47, Ronny Hanssen wrote: > > >>I personally is not vary attracted to adding methods to the enum >>itself that is supposed to do something based on a given enum. That >>enum is only the test for if something is to be done or not. The >>actual method is most often something related to the object doing the >>switch. > > > I agree on the latter, but not the former. I''m sure there is an elegant > way to polymorphically, i.e. based on the enum instance, trigger an > action that is dependent on the context. > > Here''s an example (maybe someone recognizes the movie I just watched). > Say, you have enumerations for various medical qualities, such as > BloodPh, with instances for high, medium, low. Now, as it happens, > people with medium blood pH will die instantly when exposed to > Andromeda, the others survive. With Enumerations as I introduced them, > you could piggyback this information like this > > class <<BloodPh[:high] > def survives_andromeda; true; end > end > > class <<BloodPh[:medium] > def survives_andromeda; false; end > end > > class <<BloodPh[:low] > def survives_andromeda; true; end > end > > Let''s say we do something similar for other enums, then we could write > this method > > class Patient > def will_survive > survives_common_cold && survives_andromeda && ... > end > def survives_andromeda > blood_ph.survives_andromeda > end > ... > end > > Well, this is surely not the best possible example, but I hope you see > what I mean. > > Michael >