1

Update: I've simplified my question; you can see the full history by checking out my editing revisions. Thanks to iain and bernardk for getting me this far.


I want to load carrierwave functionality into an instance of my User < ActiveRecord::Base model.

require 'uploaders/avatar_uploader'

module HasAnAvatar
  def self.extended(host)
    if host.class == Class
      mount_uploader :avatar, AvatarUploader
    else
      class << host
        mount_uploader :avatar, AvatarUploader
      end
    end
  end
end

Executing:

(user = User.first).extend(HasAnAvatar).avatar

Results in:

NoMethodError: undefined method new' for nil:NilClass from /Users/evan/.rbenv/versions/1.9.3-p286/lib/ruby/gems/1.9.1/gems/carrierwave-0.6.2/lib/carrierwave/mount.rb:306:in uploader'

I suspect the problem is that mount_uploader in HasAnAvatar is not being invoked properly on the eigenclass for user, such that the uploaders hash isn't populated.

Any thoughts on how to get this working?


Here is an example Rails app for this issue: https://github.com/neezer/extend_with_avatar_example

Community
  • 1
  • 1
neezer
  • 19,720
  • 33
  • 121
  • 220
  • In addition to my answer, you also need to understand the method search mechanism. Please read http://stackoverflow.com/questions/14088513/ruby-module-mixins-and-blocks-confusing/14090170#14090170 – BernardK Jan 05 '13 at 18:30
  • I can't reproduce the problem. Do you have a link to `'uploaders/avatar_uploader'` ? And also `mount.rb`. – BernardK Jan 05 '13 at 21:56
  • @BernardK `mount.rb` is part of `carrierwave` (inside the gem). Taking a fresh look at my `avatar_uploader` class to see if it's doing something a stock `carrierwave` uploader isn't... – neezer Jan 05 '13 at 22:09
  • @BernardK Here is an example Rails app that reproduces this issue: https://github.com/neezer/extend_with_avatar_example (sorry about the readme--Github refuses to read it as a markdown file and keeps insisting its and rdoc... weird) – neezer Jan 05 '13 at 22:44

5 Answers5

2

Here are two ways you can can include a module into an instance (which is to basically extend the instance's eigenclass). I don't think this is the best answer to your problem though, even if it may answer the question (in part).

class A
end
# => nil

module B
  def blah
    "Blah!"
  end
end
# => nil

a = A.new
=> #<A:0x0000010086cdf0>

a.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010086cdf0>

class << a
  include B
end

a.blah
# => "Blah!"

b = A.new
# => #<A:0x0000010083b818>

b.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010083b818>

b.extend B
# => #<A:0x0000010083b818>

b.blah
# => "Blah!"

c.blah
# NoMethodError: undefined method `blah' for #<A:0x0000010085eed0>

From your edit:

module Pacifiable
  def pacified_with(mechanism)
    class_eval do
      define_method(:"pacified_with_#{mechanism}?") { true }
    end
  end
end
# => nil

class JellyFish
  define_method(:is_squishy?) { true }
end
# => #<Proc:0x00000100850448@(irb):10 (lambda)>

class Lobster
  extend Pacifiable
  pacified_with :polar_bear
  define_method(:is_squishy?) { false }
end
# => #<Proc:0x00000100960540@(irb):16 (lambda)>

lobster = Lobster.new
# => #<Lobster:0x0000010095aa50>

lobster.pacified_with_polar_bear?
# => true

jelly = JellyFish.new
# => #<JellyFish:0x00000100951108>

jelly.pacified_with_polar_bear?
# NoMethodError: undefined method `pacified_with_polar_bear?' for #<JellyFish:0x00000100951108>

class << jelly
    extend Pacifiable
    pacified_with :polar_bear
  end
# => #<Proc:0x0000010093ddd8@(irb):4 (lambda)>

jelly.pacified_with_polar_bear?
# => true

big_jelly = JellyFish.new
# => #<JellyFish:0x0000010091ad38>

big_jelly.pacified_with_polar_bear?
# NoMethodError: undefined method `pacified_with_polar_bear?' for #<JellyFish:0x0000010091ad38>
ian
  • 12,003
  • 9
  • 51
  • 107
  • Totally understand your examples here, and extending the eigenclass seems to get me further along towards what I'm after, though it's still not quite working with CarrierWave. [Here's what I have currently](https://gist.github.com/4463528); any thoughts? – neezer Jan 05 '13 at 20:41
  • @neezer `if self.class == Class` should be `if host.class == Class`, because here self is always the module `HasAnAvatar`. To track the value of self, insert `puts #{self}` statements. In my version of `Pacifiable`, the value of self changes after `def host.pacified_with`. This is well explained in http://pragprog.com/book/ppmetr/metaprogramming-ruby. – BernardK Jan 05 '13 at 21:12
  • @bernardk You're right about the `host.class` thing; missed that. Will update. Still get the same error, though. – neezer Jan 05 '13 at 21:14
1

From what I know of Ruby classes, once I include a module into a class, ... but not retroactively alter any existing instances of User.

On the contrary, an include/extend immediately affects all existing instances, because it is a question of pointer between the class and its superclass. See How does Inheritance work in Ruby? and also the links inside.

module HasAnAvatar
    def m
        puts 'in HasAnAvatar#m'
    end
end

class AvatarUploader; end

class User
end

user = User.new
print 'user.respond_to?(:m) ? ';         puts user.respond_to?(:m) ? 'yes' : 'no'
print '1) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'

class User
    include HasAnAvatar
end

print 'user.respond_to?(:m) ? ';         puts user.respond_to?(:m) ? 'yes' : 'no'
print '2) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'

module HasAnAvatar
  def self.included(base)
    puts "#{self} included into #{base}"
#    base.mount_uploader :avatar, AvatarUploader
    base.send(:attr_reader, :avatar)
  end
end

class User
    include HasAnAvatar
end

print '3) user.respond_to?(:avatar) ? '; puts user.respond_to?(:avatar) ? 'yes' : 'no'
print 'user.avatar : '; p user.avatar
print 'user.avatar.class : '; p user.avatar.class

user.instance_variable_set(:@avatar, AvatarUploader.new)
print 'after set, user.avatar : '; p user.avatar
print 'user.avatar.class : '; p user.avatar.class

Execution (Ruby 1.9.2) :

$ ruby -w t.rb 
user.respond_to?(:m) ? no
1) user.respond_to?(:avatar) ? no
user.respond_to?(:m) ? yes
2) user.respond_to?(:avatar) ? no
HasAnAvatar included into User
3) user.respond_to?(:avatar) ? yes
user.avatar : nil
user.avatar.class : NilClass
after set, user.avatar : #<AvatarUploader:0x007fcc2b047cf8>
user.avatar.class : AvatarUploader

So included methods immediately become available to all existing instances.
Why does user.avatar answer nil ? Because instance variables belong to ... single instances. See Why are symbols in Ruby not thought of as a type of variable? In this case, the old user was not assigned an avatar.

But why do you get user2.avatar.class #=> AvatarUploader. I suppose that, behind the scene, base.mount_uploader :avatar, AvatarUploader does something such that :avatar is not an accessor of a corresponding instance variable, or defines an initialize method which starts to set this variable into new instances.


Here is a solution for the second example :

module Pacifiable
    def self.extended(host)
        puts "#{host} extended by #{self}"
        def host.pacified_with(mechanism)
            @@method_name = "pacified_with_#{mechanism}?"
            puts "about to define '#{@@method_name}' into #{self} of class #{self.class }"
            if self.class == Class
            then # define an instance method in a class
                define_method(@@method_name) { true }
            else # define a singleton method for an object
                class << self
                    define_method(@@method_name) { true }
                end
            end
        end
    end
end

class JellyFish
  define_method(:is_squishy?) { true }
end

class Lobster
  extend Pacifiable
  pacified_with :polar_bear
  define_method(:is_squishy?) { false }
end

a_lobster = Lobster.new
print 'a_lobster.pacified_with_polar_bear? '; p a_lobster.pacified_with_polar_bear?
print 'a_lobster.is_squishy? '; p a_lobster.is_squishy?

jelly = JellyFish.new

### Add functionality to instance
#
## what I want:
#
jelly.extend(Pacifiable)
jelly.pacified_with(:polar_bear)
print 'jelly.pacified_with_polar_bear? '; p jelly.pacified_with_polar_bear? #=> true  

Execution :

Lobster extended by Pacifiable
about to define 'pacified_with_polar_bear?' into Lobster of class Class
a_lobster.pacified_with_polar_bear? true
a_lobster.is_squishy? false
#<JellyFish:0x007fcc2b047640> extended by Pacifiable
about to define 'pacified_with_polar_bear?' into #<JellyFish:0x007fcc2b047640> of class JellyFish
jelly.pacified_with_polar_bear? true


About jelly.class.pacified_with(:polar_bear) : the mistake was to call class, which answers the class JellyFish; what you want is the singleton class of the instance object jelly. Once a method is defined in the singleton class of an object, you can send it directly to the object. The same applies to classes, which are instances of Class. Once a method is defined in the singleton class of a class, you can send it directly to the class. We call them class methods, they are actually instance methods of the singleton class of the class. Ouf !


Last OR : as explained, if you extend the class, it is valid for all existing instances. Thus you have to extend individual instances :

class JellyFromTheBigBlueSea
  def find
    puts 'in JellyFromTheBigBlueSea#find'
    jelly = JellyFish.new
    jelly.extend(Pacifiable)
    jelly.pacified_with :polar_bear
    jelly
  end
end

class JellyFromAnIsolatedCove
  def find
    puts 'in JellyFromAnIsolatedCove#find'
    JellyFish.new
  end
end

normal_jelly   = JellyFromTheBigBlueSea.new.find
ignorant_jelly = JellyFromAnIsolatedCove.new.find

## what I want:
#
print 'normal_jelly.pacified_with_polar_bear? ';   p normal_jelly.pacified_with_polar_bear?
print 'ignorant_jelly.pacified_with_polar_bear?' ; p ignorant_jelly.pacified_with_polar_bear?

Execution :

in JellyFromTheBigBlueSea#find
#<JellyFish:0x007fb5d9045060> extended by Pacifiable
about to define 'pacified_with_polar_bear?' into #<JellyFish:0x007fb5d9045060> of class JellyFish
in JellyFromAnIsolatedCove#find
normal_jelly.pacified_with_polar_bear? true
ignorant_jelly.pacified_with_polar_bear?t.rb:109:in `<main>': undefined method `pacified_with_polar_bear?' for #<JellyFish:0x007fb5d9044d18> (NoMethodError)
Community
  • 1
  • 1
BernardK
  • 3,674
  • 2
  • 15
  • 10
  • Thanks for the detailed answer; I think I understand your point about the singleton class from my second example, but I'm not sure I follow your point about my first exampe (with Carrierwave). Specifically, not sure what you mean by "after set" in you execution block. Could you clarify? – neezer Jan 05 '13 at 19:43
  • @neezer Just to emphasize that the instance variable `@avatar` belongs to this single instance user, and can be set anytime individually. What I don't understand is your `user.avatar.class #=> AvatarUploader` for all users. If `avatar` where an attribute_reader, it should answer nil or whatever had been assigned to it. What is shown _"after set"_ with `p user.avatar.class` is that if `avatar` is an attribute_reader, and if and only if I **set** the value myself, then the value returned is `AvatarUploader`. (Continued ...) – BernardK Jan 05 '13 at 20:31
  • @neezer ... When you say I wish `user.avatar.class #=> AvatarUploader` and `user2.avatar.class #=> String or Nil` and seem to make it depend on `User.send :include, HasAnAvatar`, I say no, it depends only on the assignment to each individual user instance. HTH :) – BernardK Jan 05 '13 at 20:36
  • CarrierWave (I think) [overlays logic on top of the attribute_reader, then super's down into the original behavior](https://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/mount.rb#L169). I'm [making progress extending the eigenclass of the user](http://stackoverflow.com/questions/14170344/locally-extend-ruby-class-or-add-new-class-defined-initialization-logic-to-an-e#comment19644755_14170925), but still not quite there yet. Any other thoughts? – neezer Jan 05 '13 at 20:47
0

Firstly there's something odd about your contexts or at least the naming. Context do not return RolePlayers. Roles exist only inside a context. The role methods are not and should not be accessible outside the context. That said the standard way of dealing with DCI in ruby is method injection. This approach is not flawless but is so far the closest to pure DCI anyone have come in Ruby. There's an experimental library called alias_dci that might help.

EDIT There now is a gem that makes injectionless DCI possible in Ruby. It's based on the work gone into Marvin, the first language to support injectionless DCI. The gem is called Moby and can be installed with

gem install Moby

it's currently still somewhat experimental but the smoke test of being able to implement the DCI examples from fullOO has been passed

Rune FS
  • 21,497
  • 7
  • 62
  • 96
  • Still learning DCI here, so I've probably not got it quite right yet, as far as the actual pattern is concerned. I'm generally returning RolePlayers so that I can use Rails `respond_with` them to keep my controllers super dry. I'll take a look at that library you suggested too. – neezer Jan 05 '13 at 10:18
  • @neezer it might work and do what you expect but it loosers on one of the main points of DCI. That the code be in one single location for each operation and not spread over multiple classes – Rune FS Jan 05 '13 at 16:06
  • Trying to implement Jim Gay's [Clean Ruby](http://clean-ruby.com/) suggestion, Loc. 1505 (in Kindle, MOBI edition), version 0.5 – neezer Jan 05 '13 at 20:11
  • I haven't read Jim's book but debated only with him an he's usually spot on when it come to DCI. So could it be you are changing the example to fit in another puzzle? – Rune FS Jan 06 '13 at 07:57
  • We might also be talking past each other: I'm not intending to use the RolePlayer as the input to some other function: in this case, it is the output of the context that's being fed directly to a Rails responder and returned to the client as final output. I was inspecting the output in my posted question to determine if the output was correct, so it could be that my nomenclature in the question didn't match the intent of what I was actually aiming to accomplish. – neezer Jan 06 '13 at 09:18
0
  1. First of all, the way with instance extend calls is working (See end of the post).

    1. Then you should take into account some opinions that this is bad for performance
    2. And at last, according to sources, it should work as expected. So I suggest you to wear your debug hat and try to see what exactly happens on user.extend part. For example, see if mount_uploader uses Carrierwave::Mount#mount_uploader method, as there are defined 'Uploader' everrides.

1.9.3p327 :001 > class A
1.9.3p327 :002?>   def foo
1.9.3p327 :003?>     '42'
1.9.3p327 :004?>     end
1.9.3p327 :005?>   end
 => nil 
1.9.3p327 :006 > A.new.foo
 => "42" 
1.9.3p327 :011 > module Ext
1.9.3p327 :012?>   def foo
1.9.3p327 :013?>     'ext'
1.9.3p327 :014?>     end
1.9.3p327 :015?>   end
 => nil 
1.9.3p327 :016 > class AFancy
1.9.3p327 :017?>   def call
1.9.3p327 :018?>     a = A.new
1.9.3p327 :019?>     a.extend Ext
1.9.3p327 :020?>     a
1.9.3p327 :021?>     end
1.9.3p327 :022?>   end
 => nil 
1.9.3p327 :023 > a1 = A.new
 => #<A:0x00000000e09b10> 
1.9.3p327 :024 > a2 = AFancy.new.call
 => #<A:0x00000000e17210> 
1.9.3p327 :025 > a3 = A.new
 => #<A:0x00000000e1bd38> 
1.9.3p327 :026 > [a1, a2, a3].map(&:foo)
 => ["42", "ext", "42"]
Mark Huk
  • 2,379
  • 21
  • 28
  • Hmm, your example is only using instance methods, not class methods that define new methods upon instantiation, like Carrierwave does. Maybe I'll have to come up with a more illustrative example... – neezer Jan 05 '13 at 10:36
  • It doesnt defines new methods on instantiation. As far as I see, it just creates new Module with overrides and includes that. All that happens on `mount_*` call, i.e. at class eval time once on app start. In your case it wont be once, but still seems no difference. – Mark Huk Jan 05 '13 at 11:05
  • "at class eval time once on app start": but that only is the case if my `ActiveRecord::Base` model includes the call to `mount_uploader`, which I'm trying to avoid (thus my question). I want to be able to pass a user object around internally without always having the CarrierWave avatar methods accompany it, so I'm trying to figure out how to include them only when I need them. – neezer Jan 05 '13 at 11:29
  • Added another example above, which (hopefully) makes more sense. Could you take a look? – neezer Jan 05 '13 at 11:34
0

Ok, I think I found out what was causing my issues...

In https://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/mount.rb, in the definition of CarrierWave::Mount::Mounter, there are three references to record.class. This rightly references back to the main User class, which doesn't have the extended methods I loaded into the user's metaclass. So, I changed those to this: https://gist.github.com/4465172, and it seemed to work.

Also seems to continue to work if used normally like in the CarrierWave docs, so that's good too. Will continue testing it, though.

neezer
  • 19,720
  • 33
  • 121
  • 220
  • I was able to reproduce the error. This mount.rb is a wonderful piece of Ruby metaprogramming ! A class_eval-ed method calls super, no superclass there but a module which will be included soon but doesn't exist yet because it will be entirely class_eval-ed ! Well done ! – BernardK Jan 06 '13 at 18:15