DSL - class-level methods that affect instances

Discussion in 'Ruby' started by Daniel Waite, May 2, 2008.

  1. Daniel Waite

    Daniel Waite Guest

    I'm having a problem of design. I've tried a few configurations of class
    collaboration and responsibility, but they all end up feeling messy.

    I'm looking at these (either or, my original attempt was with a module,
    but attr_accessor *looks* like it does what I want):

    class C < Translator::Base
    translate 'this', :to => 'that'
    end

    class C
    include Translator
    translate 'this', :to => 'that'
    end

    With the expectation that...

    c = C.new
    c.translations

    Would yield a hash of the from and to values.

    I have no idea how to break the barrier between class and instance. :(

    Any ideas?
    --
    Posted via http://www.ruby-forum.com/.
     
    Daniel Waite, May 2, 2008
    #1
    1. Advertising

  2. Daniel Waite

    Ken Bloom Guest

    On Thu, 01 May 2008 18:07:11 -0500, Daniel Waite wrote:

    > I'm having a problem of design. I've tried a few configurations of class
    > collaboration and responsibility, but they all end up feeling messy.
    >
    > I'm looking at these (either or, my original attempt was with a module,
    > but attr_accessor *looks* like it does what I want):
    >
    > class C < Translator::Base
    > translate 'this', :to => 'that'
    > end
    >
    > class C
    > include Translator
    > translate 'this', :to => 'that'
    > end
    >
    > With the expectation that...
    >
    > c = C.new
    > c.translations
    >
    > Would yield a hash of the from and to values.
    >
    > I have no idea how to break the barrier between class and instance. :(


    Use class variables (the @@ variety).

    class Translator::Base
    def self.translate word, params
    @@translations ||= {}
    @@translations[word]=params[:to]
    end
    def translations
    @@translations ||= {}
    end
    end

    class C < Translator::Base
    translate 'a', :to => 'b'
    translate 'c', :to => 'd'
    end

    C.new.translations #=> {"a"=>"b", "c"=>"d"}


    --
    Ken (Chanoch) Bloom. PhD candidate. Linguistic Cognition Laboratory.
    Department of Computer Science. Illinois Institute of Technology.
    http://www.iit.edu/~kbloom1/
     
    Ken Bloom, May 2, 2008
    #2
    1. Advertising

  3. Daniel Waite

    Daniel Waite Guest

    Ken Bloom wrote:
    > On Thu, 01 May 2008 18:07:11 -0500, Daniel Waite wrote:
    >
    >> class C
    >>
    >> I have no idea how to break the barrier between class and instance. :(

    >
    > Use class variables (the @@ variety).
    >
    > class Translator::Base
    > def self.translate word, params
    > @@translations ||= {}
    > @@translations[word]=params[:to]
    > end
    > def translations
    > @@translations ||= {}
    > end
    > end
    >
    > class C < Translator::Base
    > translate 'a', :to => 'b'
    > translate 'c', :to => 'd'
    > end
    >
    > C.new.translations #=> {"a"=>"b", "c"=>"d"}


    That works! Except it shares translations across all subclasses.

    module Translator
    class Base

    def self.translate word, params
    @@translations ||= {}
    @@translations[word]=params[:to]
    end

    def translations
    @@translations ||= {}
    end

    end
    end

    class C < Translator::Base

    translate 'a', :to => 'b'
    translate 'c', :to => 'd'

    end

    class D < Translator::Base

    translate 'this', :to => 'that'

    end

    irb(main):028:0> c = C.new
    => #<C:0x4d4e0>
    irb(main):029:0> c.translations
    => {"a"=>"b", "c"=>"d", "this"=>"that"}

    While I understand the behavior, its undesirable. I'm going to dust off
    Ruby for Rails...
    --
    Posted via http://www.ruby-forum.com/.
     
    Daniel Waite, May 2, 2008
    #3
  4. Hi --

    On Fri, 2 May 2008, Daniel Waite wrote:

    > I'm having a problem of design. I've tried a few configurations of class
    > collaboration and responsibility, but they all end up feeling messy.
    >
    > I'm looking at these (either or, my original attempt was with a module,
    > but attr_accessor *looks* like it does what I want):
    >
    > class C < Translator::Base
    > translate 'this', :to => 'that'
    > end
    >
    > class C
    > include Translator
    > translate 'this', :to => 'that'
    > end
    >
    > With the expectation that...
    >
    > c = C.new
    > c.translations
    >
    > Would yield a hash of the from and to values.
    >
    > I have no idea how to break the barrier between class and instance. :(


    I'd say: don't think of it as any more of a barrier than exists
    between any other two objects. Every object, including a class object,
    has the right to maintain state, and to decide what it will and will
    not expose. There's no privileged relationship between c and i just
    because i is an instance of c.

    (I know that sounds kind of doctrinaire, but I like to lay it on thick
    since it usually takes some doing to chip away at the presumptive
    class/instance special relationship :)

    As Ken suggested, class variables break through the class/instance
    boundaries. That's one of the reasons I don't like them :) You can
    certainly do it that way; I'll also show you another possibility,
    based on engineering all the objects concerned so that they query each
    other where necessary:

    module Translator
    attr_reader :translations

    def translate(target, actions)
    @translations ||= []
    @translations.push("Translating #{target} to #{actions[:to]}")
    end
    end

    class C
    extend Translator

    def translations
    self.class.translations
    end

    translate "this", :to => "that"
    end

    p C.new.translations # ["Translating this to that"]


    David

    --
    Rails training from David A. Black and Ruby Power and Light:
    INTRO TO RAILS June 9-12 Berlin
    ADVANCING WITH RAILS June 16-19 Berlin
    INTRO TO RAILS June 24-27 London (Skills Matter)
    See http://www.rubypal.com for details and updates!
     
    David A. Black, May 2, 2008
    #4
  5. Daniel Waite

    Ken Bloom Guest

    On Thu, 01 May 2008 19:55:45 -0500, Daniel Waite wrote:

    > Ken Bloom wrote:
    >> On Thu, 01 May 2008 18:07:11 -0500, Daniel Waite wrote:
    >>
    >>> class C
    >>>
    >>> I have no idea how to break the barrier between class and instance. :(

    >>
    >> Use class variables (the @@ variety).
    >>
    >> class Translator::Base
    >> def self.translate word, params
    >> @@translations ||= {}
    >> @@translations[word]=params[:to]
    >> end
    >> def translations
    >> @@translations ||= {}
    >> end
    >> end
    >>
    >> class C < Translator::Base
    >> translate 'a', :to => 'b'
    >> translate 'c', :to => 'd'
    >> end
    >>
    >> C.new.translations #=> {"a"=>"b", "c"=>"d"}

    >
    > That works! Except it shares translations across all subclasses.
    >
    > module Translator
    > class Base
    >
    > def self.translate word, params
    > @@translations ||= {}
    > @@translations[word]=params[:to]
    > end
    >
    > def translations
    > @@translations ||= {}
    > end
    >
    > end
    > end
    >
    > class C < Translator::Base
    >
    > translate 'a', :to => 'b'
    > translate 'c', :to => 'd'
    >
    > end
    >
    > class D < Translator::Base
    >
    > translate 'this', :to => 'that'
    >
    > end
    >
    > irb(main):028:0> c = C.new
    > => #<C:0x4d4e0>
    > irb(main):029:0> c.translations
    > => {"a"=>"b", "c"=>"d", "this"=>"that"}
    >
    > While I understand the behavior, its undesirable. I'm going to dust off
    > Ruby for Rails...


    I couldn't remember why I didn't use @@ variables for another very
    similar metaprogramming problem I had this week. Now you've reminded why.

    This version uses instance variables on the singleton class (which is
    different from using @@ class variables)

    class Translator::Base
    def self.translate word, params
    @translations ||= {}
    @translations[word]=params[:to]
    end
    def translations
    self.class.instance_variable_get:)@translations) || {}
    end
    end

    class C < Translator::Base
    translate 'a', :to => 'b'
    translate 'c', :to => 'd'
    end

    C.new.translations #=> {"a"=>"b", "c"=>"d"}

    class D < Translator::Base
    translate 'e', :to => 'f'
    end

    D.new.translations #=> {"e"=>"f"}
    C.new.translations #=> {"a"=>"b", "c"=>"d"}

    #But beware:
    class E < C
    translate 'g', :to => 'h'
    end

    E.new.translations #=> {"g"=>"h"}

    There are ways to solve this by explicitly asking for your parent class's
    translations, if you'd like to have that functionality.

    --Ken

    --
    Ken (Chanoch) Bloom. PhD candidate. Linguistic Cognition Laboratory.
    Department of Computer Science. Illinois Institute of Technology.
    http://www.iit.edu/~kbloom1/
     
    Ken Bloom, May 2, 2008
    #5
  6. Hi --

    On Fri, 2 May 2008, Ken Bloom wrote:

    > On Thu, 01 May 2008 19:55:45 -0500, Daniel Waite wrote:
    >
    >> Ken Bloom wrote:
    >>> On Thu, 01 May 2008 18:07:11 -0500, Daniel Waite wrote:
    >>>
    >>>> class C
    >>>>
    >>>> I have no idea how to break the barrier between class and instance. :(
    >>>
    >>> Use class variables (the @@ variety).
    >>>
    >>> class Translator::Base
    >>> def self.translate word, params
    >>> @@translations ||= {}
    >>> @@translations[word]=params[:to]
    >>> end
    >>> def translations
    >>> @@translations ||= {}
    >>> end
    >>> end
    >>>
    >>> class C < Translator::Base
    >>> translate 'a', :to => 'b'
    >>> translate 'c', :to => 'd'
    >>> end
    >>>
    >>> C.new.translations #=> {"a"=>"b", "c"=>"d"}

    >>
    >> That works! Except it shares translations across all subclasses.
    >>
    >> module Translator
    >> class Base
    >>
    >> def self.translate word, params
    >> @@translations ||= {}
    >> @@translations[word]=params[:to]
    >> end
    >>
    >> def translations
    >> @@translations ||= {}
    >> end
    >>
    >> end
    >> end
    >>
    >> class C < Translator::Base
    >>
    >> translate 'a', :to => 'b'
    >> translate 'c', :to => 'd'
    >>
    >> end
    >>
    >> class D < Translator::Base
    >>
    >> translate 'this', :to => 'that'
    >>
    >> end
    >>
    >> irb(main):028:0> c = C.new
    >> => #<C:0x4d4e0>
    >> irb(main):029:0> c.translations
    >> => {"a"=>"b", "c"=>"d", "this"=>"that"}
    >>
    >> While I understand the behavior, its undesirable. I'm going to dust off
    >> Ruby for Rails...

    >
    > I couldn't remember why I didn't use @@ variables for another very
    > similar metaprogramming problem I had this week. Now you've reminded why.
    >
    > This version uses instance variables on the singleton class (which is
    > different from using @@ class variables)


    I wish that class variables were $$ instead of @@. They're really a
    kind of restricted global, and they have the same qualities of sprawl
    that globals do. The @@ always makes it look like they have some kind
    of kinship with instance variables, when in fact they're practically
    the opposite, in that they open up a shared space for storing state
    whereas instance variables specifically make it possible for objects
    to control state on an individual, private (give or take the fact that
    nothing is really private in Ruby) basis.


    David

    --
    Rails training from David A. Black and Ruby Power and Light:
    INTRO TO RAILS June 9-12 Berlin
    ADVANCING WITH RAILS June 16-19 Berlin
    INTRO TO RAILS June 24-27 London (Skills Matter)
    See http://www.rubypal.com for details and updates!
     
    David A. Black, May 2, 2008
    #6
  7. Daniel Waite

    Daniel Waite Guest

    David A. Black wrote:
    > I'd say: don't think of it as any more of a barrier than exists
    > between any other two objects.


    That helps, actually. Viscerally, for now, but I'm sure the fullness of
    the stance will sink in with time.

    > (I know that sounds kind of doctrinaire, but I like to lay it on thick
    > since it usually takes some doing to chip away at the presumptive
    > class/instance special relationship :)


    I love saturated opinions. (David West all the way!) I'm re-reading your
    book right now starting from chapter 5, and yes, I can already tell I've
    lost a forgotten a lot of my understanding of how Ruby works. (I suppose
    you could argue I never understood it if I forgot...?)

    > module Translator
    > attr_reader :translations
    >
    > def translate(target, actions)
    > @translations ||= []
    > @translations.push("Translating #{target} to #{actions[:to]}")
    > end
    > end
    >
    > class C
    > extend Translator
    >
    > def translations
    > self.class.translations
    > end
    >
    > translate "this", :to => "that"
    > end
    >
    > p C.new.translations # ["Translating this to that"]


    Works beautifully. Amazing. I thought I was going to end up with some
    monolithic structure like ActiveRecord. ;)

    @Ken:

    Your solution works equally well! I like that yours is class-based so I
    don't have to call extend in the body of each inheriting class (of which
    there will be quite a few).

    I'm not worried about tertiary inheritance; I don't plan on needing it.
    (Famous last words, right?)

    Thank you both, Ken and David, very much!
    --
    Posted via http://www.ruby-forum.com/.
     
    Daniel Waite, May 2, 2008
    #7
  8. Daniel Waite

    Daniel Waite Guest

    Daniel Waite wrote:
    > Thank you both, Ken and David, very much!


    Here she is, best of both approaches!

    module Translator

    module ClassMethods
    attr_reader :translations

    def translate(from_word, options)
    @translations ||= Hash.new
    @translations[from_word] = options[:to]
    end
    end

    def self.included(receiver)
    receiver.extend(ClassMethods)
    end

    def translations
    self.class.translations
    end

    class Base
    include Translator
    end

    end

    class NewTranslator < Translator::Base

    translate 'customer_profile_id', :to => 'customerProfileId'

    end

    p NewTranslator.new.translations

    I feel a little odd putting the class Base declaration at the end of the
    module, but if it's not there it has to be outside it entirely (and it
    must still occur _after_ the code inside the module), something I don't
    really want to do.

    Anywho, thanks again for the awesome help. You guys rock!
    --
    Posted via http://www.ruby-forum.com/.
     
    Daniel Waite, May 2, 2008
    #8
  9. Andrew Stewart, May 2, 2008
    #9
    1. Advertising

Want to reply to this thread or ask your own question?

It takes just 2 minutes to sign up (and it's free!). Just click the sign up button to choose a username and then you can ask your own questions on the forum.
Similar Threads
  1. John Wohlbier
    Replies:
    2
    Views:
    399
    Josiah Carlson
    Feb 22, 2004
  2. pabbu
    Replies:
    8
    Views:
    773
    Marc Boyer
    Nov 7, 2005
  3. M. Ayhan
    Replies:
    1
    Views:
    133
    Trans
    Mar 8, 2007
  4. Kenneth McDonald
    Replies:
    5
    Views:
    387
    Kenneth McDonald
    Sep 26, 2008
  5. dc
    Replies:
    2
    Views:
    95
    Shawn Anderson
    Dec 7, 2008
Loading...

Share This Page