I thought I was making progress with Ruby but I have now found that my knowledge regarding the use of mixins is flawed.
This much simplifed code shows a class, TestClass, containing a single variable ‘@state’. This class mixes in a module, TestModule, which has a single method ‘switch_state’. Creating an instance of TestClass and repeatedly calling ‘switch_state’ works as expected - as if the method were native to TestClass.
However, TestModule also creates a Command and adds this to a Toolbar instance. The command block contains the method name ‘switch_state’. If I click on the toolbar icon it reports that the method ‘switch_state’ cannot be found.
module SomeModule
module Testbar
cmd = UI::Command.new("Test") {
switch_state
}
UI.toolbar("Testbar").add_item cmd
def switch_state
@state = @state == 0 ? 1 : 0
end
end # end of module Testbar
class TestClass
include Testbar
attr_reader :state
def initialize state
@state = state
end
end # end of class
t = TestClass.new 0
puts t.state # => 0
t.switch_state
puts t.state # => 1
t.switch_state
puts t.state # => 0
# clicking on toolbar command "Test" produces "undefined local variable or method `switch_state'"
end # end of module SomeModule
Any help that would realign my brain cells would be much appreciated.
This is a confusing technical matter about how modules, mixins and Ruby’s dynamic invocation of methods work. I hope I have it right and explain it adequately below. Likely @DanRathbun will chime in and clarify or correct me if I bungle it!
As it processes your source code, the Ruby interpreter doesn’t actually bind calls to methods. Rather, it codes a lookup of the method names it finds in your code. The lookup is within the namespace hierarchy that is active at that point in processing the source. Since the method could be added later, there is no interpretation-time error if the named method doesn’t exist at that time. Existence is resolved (and may fail) at runtime.
Now, a module can’t have instances, so it doesn’t really have instance methods you can call via the module itself. Rather, methods def’d in the module block are added to a class’s lookup system when the class includes the module. They are only accessible via instances of a class that includes the module.
Other code in the module’s code block is evaluated when the block runs, in the context of that module. It is not re-evaluated when the module is mixed into a class.
So, at the time your code creates the UI.Command and toolbar, it is running in the TestBar module’s namespace, not in TestClass’s namespace. Ruby generates the runtime code to look up and execute a method named “switch_state” in the module’s namespace because that is what was active when the code creating the Command was evaluated. But when you actually invoke the command via the toolbar, no such instance method can be found. It is accessible via a TestClass instance, but not directly via the module. So, at runtime the lookup fails and you get the error message.
Besides the other errors … the above is frivolous because state() does nothing more than return @state, so the above method’s code is really @state = @state.
The def is also wrong in that there cannot be spaces in method names, and if you intended that state is an argument, then you need parenthesis around the argument list for method definitions. Ie …
def initialize(state)
The main error (as Steve will likely say) is that the mixin module’s @state variable is distinct from the class’ @state variable.
(Move the attr_reader :state line into the mixin module.) EDIT: Ie, moving the statement will cause the @state attribute (and it’s reader method) to be properly created in the mixin module, and therefore available to any class that mixes it in.
Basically mixing in using include allows the “mixee” to access the mixin’s instance variables, instance methods and constants. That’s it.
In your test snippet, the local reference cmd would not be accessible. (But as shown below there is not much cause to try use them with a mixin anyway.)
Note that mixin modules affect the “mixee” differently depending upon whether they are used with include or extend.
I agree with this, but further more the mixin module is not the place for a UI object definition like UI commands, toolbars and menu items. We usually put them inside a load guard block at the bottom of the extension’s submodule.
In your snippet "SomeModule" would be the extension submodule. This means your snippet is lacking a top level namespace module (which needs to be globally unique.) This will separate all of your extension submodules from everyone else’s extension submodules.
You could use your name "module PhilipBlenkinsop" or some fictitious business name or trademarkable brandname.
Please do not outdent (skip indents), it creates less readable code.
Anyway … for creating UI elements, since you will likley need to reload your code files during development, put them inside a load guard if block at the bottom of the extension submodule. Viz:
module PhilipBlenkinsop
module SomeExtension
# Definition of extension constants holding values
TOOLBAR_NAME ||= "Testbar"
# Definition (or persistence loading) of extension variables
# Definition of extension submodules and extension specific classes
# Definition of extension methods
if !defined(@ui_loaded)
@ui_loaded = true
# Definition of UI objects
end
end
end
The above scenario of “sections” can be divided into multiple files. (This has been covered in other topics. Please do a search.)
Steve - what I take away from this is that there is a solid wall between a module’s methods so I cannot apply them to variables in an instance of a class. I’m stumped how to get around this. If the Command in the module changed a variable in the module I would happily read that variable from methods in the class instance but I don’t think this is possible.
Dan
Besides the other errors … the above is frivolous because state() does nothing more than return @state , so the above method’s code is really @state = @state .
er, surely not? To quote your favorite (Pickaxe) book)
Rule: Methods are defined with the keyword def followed
by the method name and parameters where parameter are
optionally enclosed in parentheses
Not, perhaps, a preferred style but it does run quite happily I can assure you. Please bear in mind that the code quoted is not the code I am running. It is stripped down as far as I can take it. In practice I have an isolating module around all of my code as you suggest to keep it distinct from other extensions.
If you look back at the extension I wrote over a year ago (Toggler.rbz) all of the code was in modules and so I didn’t have this problem. In this case I need to isolate some code in a class instance but I need to change state of a variable within it from a Toolbar Command.
I would like to change the value or state of a variable somewhere in my code using a Command in a Toolbar and be able to read that value in a class instance (preferably not using a global variable)
Well as you see here, it causes confusion in reading your code. It is best to always use parenthesis in method definitions, and when calling all methods except global methods (those defined in Kernel, BasicObject, Object, Module and Class.)
Omitting parenthesis and using non-standard indentation causes misread (as above) and we “spin out wheels” trying to help you.
A mixin is not (usually) the way to expose an instance state variable.
Probably better would be just what we call a wrapper method in a outer module. The module holds a @instance reference to the instance, the module defines a wrapper method that calls the instances attribute reader method, and the command calls the module’s wrapper method.
Where the Namespace module is already predefined. (Ie, saving an ident here.)
module Namespace::SomeExtension
extend self
CMD ||= {} # Hash to hold UI command references
@instance = nil unless defined?(@instance)
class TestClass
attr_reader :state
def initialize( state )
@state = state
end
def switch_state
@state = @state == 0 ? 1 : 0
end
end # end of class
def get_state
@instance ? @instance.state : 0
end
def create_instance(state = 0)
@instance = TestClass.new(state)
end
def toggle
@instance.switch_state if @instance
end
if !defined(@ui_loaded)
@ui_loaded = true
# Definition of UI objects
CMD["Toggle"] = UI::Command.new("Toggle Test") {
toggle()
}.set_validation_proc {
get_state() == 0 ? MF_UNCHECKED | MF_CHECKED
}
# Perhaps more command objects here ?
# Define and load the toolbar:
TOOLBAR = UI.toolbar("Testbar")
CMD.each { |key,cmd|
TOOLBAR.add_item(cmd)
}
end
end
True, but a module is an instance of class Module.
But it can if it uses the extend self trick to extend itself with itself. (See example of above.)
Yes it is possible. This would be the inverse of the example above. Ie, the state would be held in the module, and the instance would call a module method by qualifying the method call with the module name.
So the module method definition would be (something like) …
def self.get_state
@state
end
… and the instance would retrieve the value via a ModuleName.get_state call.
(Note that the extend self trick will also cause public singleton methods to be created, so even in the example above the instance can call the module’s methods with qualification. However the definitions in that example just access the instance’s getter methods. Their definitions would be different if the state was held in the module rather than the instance.)
The main reason why we usually hold state in an instance object, is because of the Mac edition’s plurality of open model instances. The instances are unique to the model that “owns” them (or the model that the instance is associated with.)
If the state in question is the same regardless of all models (ie, likely a general extension option,) then we usually hold the state in the extension module itself. (I myself prefer to have such options in a hash object at the extension module level.)
Which I think is the heart of the question, and not mixins.
You could use class variable like “@@state”, in the module. If you want to reference a instance variable, it is not only possible that it has gone out of scope, but there could be more than one instance of your class (shouldn’t occur for Tools). Instead of putting that in the class definition, put it at the module level. Create a module method that returns it.
module SomeModule
module Testbar
@@state = 0
cmd = UI::Command.new("Test") {
switch_state
}
UI.toolbar("Testbar").add_item cmd
def switch_state
@@state = @@state == 0 ? 1 : 0
end
def get_state
@@state
end
end # end of module Testbar
class TestClass
# . . .
No need to mixin another module that the class already resides in.
You don’t even need an instance state variable called “@state”, unless it is necessary for when there is more than one instance and they can be different.
I didn’t use the getter method “get_state”, but you may need to access it from a different module, like a toolbar module. It is called a “class variable”, but I also assign them in modules, which eliminates the need for global variables. You can access it with:
testbar_state = SomeModule::Testbar.get_state
# It looks like this toolbar doesn't need a getter.
# You could call switch_state with:
SomeModule::Testbar.switch_state
I’m in a middle of studying Ruby, I always wan to do “trial an fail”… to see how it is…
So, my first imagination, as usual quick and dirty code snippet:
(I’m indicating the state with the icon “color” and with console output.)
module AuthorTopLevelNamespace
module AuthorPlugin
extend self
@@loaded = false unless defined?(@@loaded)
def start_tool
Sketchup.active_model.select_tool( @tool = Testtool.new ) unless @@run
end
def change_state
@tool.change_state if @@run
end
unless @@loaded
@@run = false
@@state = false
cmd1 = UI::Command.new("Start"){start_tool}
cmd1.tooltip = "Start"
cmd1.set_validation_proc {
if @@run
MF_DISABLED | MF_CHECKED
else
MF_ENABLED | MF_UNCHECKED
end
}
cmd2 = UI::Command.new("Change"){change_state}
cmd2.tooltip = "Change"
cmd2.set_validation_proc {
if @@state
MF_ENABLED | MF_UNCHECKED
else
MF_DISABLED | MF_CHECKED
end
}
toolbar1 = UI::Toolbar.new('State Test')
toolbar1.add_item(cmd1)
toolbar1.add_item(cmd2)
toolbar1.restore
@@loaded = true
end
class Testtool
include AuthorPlugin
def activate
@@run = true
puts "activated"
end
def deactivate(view)
@@run = false
@@state = false
puts "deactivated"
end
def onLButtonDoubleClick(flags, x, y, view)
puts "onLButtonDoubleClick:"
@@state = !@@state
puts "Change from Tool, state: #{@@state}"
end
def change_state
@@state = !@@state
puts "Change from module, state: #{@@state}"
end
end
end
end