Let’s take a look at a concrete example of a problem mentioned earlier, namely, the inability to use marshaling to save and load a singleton. In singleton_m.rb I have created an instance of Object, ob
, and then extended it in the form of a singleton class that is given the additional method, xxx
:
singleton_m.rb
ob = Object.new class << ob def xxx( aStr ) @x = aStr end end
The problem arises when I try to save this data to disk using Marshal.dump
. Ruby displays an error message: “singleton can’t be dumped (TypeError).”
Before considering how you might deal with this, let’s briefly take a look at how YAML would cope in this situation. The program singleton_y.rb tries to save the singleton that I created a moment ago using YAML.dump
, and, unlike Marshal.dump
, it succeeds—well, sort of:
singleton_y.rb
# YAML version of singleton-save ob.xxx( "hello world" ) File.open( 'test.yml', 'w' ){ |f| YAML.dump( ob, f ) } ob.xxx( "new string" ) File.open( 'test.yml' ){ |f| ob = YAML.load(f) }
If you look at the YAML file that is saved, test.yml, you’ll find that it defines an instance of a plain-vanilla Object to which a variable named x
is appended that has the string value hello world
:
--- !ruby/object x: hello world
That’s all well and good. However, when you reconstruct the object by loading the saved data, the new ob
will be a standard instance of Object, which happens to contain an additional instance variable, @x
. Since it is no longer the original singleton, this ob
will not have access to any of the methods (here the xxx
method) defined in that singleton. So, although YAML serialization is more permissive about saving and loading data items that were created in a singleton, it does not automatically re-create the singleton itself when the saved data is reloaded.
Let’s now return to the Marshal version of this program. The first thing I need to do is find a way of at least making it save and load data items. Once I’ve done that, I’ll try to figure out how to reconstruct singletons on reloading.
To save specific data items, I can define the marshal_dump
and marshal_load
methods as explained earlier (see limit_m.rb). These should normally be defined in a class from which the singleton derives, not in the singleton itself. This is because, as already explained, when the data is saved, it will be stored as a representation of the class from which the singleton derives. This means that although you could indeed add marshal_dump
to a singleton derived from class X, when you reconstruct the object, you will be loading data for an object of the generic type X, not of the specific singleton instance.
This code creates a singleton, ob
, of class X, saves its data, and then re-creates a generic object of class X:
singleton_m2.rb
class X def marshal_dump [@x] end def marshal_load(data) @x = data[0] end end ob = X.new class << ob def xxx( aStr ) @x = aStr end end ob.xxx( "hello" ) p( ob ) File.open( 'test2.sav', 'w' ){ |f| Marshal.dump( ob, f ) } ob.xxx( "new string" ) p( ob ) File.open( 'test2.sav' ){ |f| ob = Marshal.load(f) } p( ob )
The code here uses Marshal.dump
to save an object, ob
, of class X and then calls the singleton method, xxx
, to assign a different string to the @x
variable before reloading the saved data using Marshal.load
and using this data to re-create the object. The contents of ob
are displayed using p()
before it is saved, then again after a new string is assigned to it, and finally once again when it is reloaded. This lets you verify that @x
is assigned the value that was saved when the reloaded object is reconstructed:
#<X:0x2b86cc0 @x="hello"> # value when saved #<X:0x2b86cc0 @x="new string"> # new value then assigned #<X:0x2b869f0 @x="hello"> # value after saved data loaded
In terms of the data it contains, the object saved and the object reloaded are identical. However, the object that is reloaded knows nothing about the singleton class. The method xxx
that the singleton class contains forms no part of the reconstructed object. The following, then, would fail:
ob.xxx( "this fails" )
This Marshal version of the code is equivalent to the YAML version given earlier. It saves and restores the data correctly, but it does not reconstruct the singleton. How, then, is it possible to reconstruct a singleton from saved data? There are, no doubt, many clever and subtle ways in which this might be accomplished. I shall, however, opt for a very simple technique:
singleton_m3.rb
FILENAME = 'test2.sav' class X def marshal_dump [@x] end def marshal_load(data) @x = data[0] end end ob = X.new # a) if File exists, load data into ob - a generic X object if File.exists?(FILENAME) then File.open(FILENAME){ |f| ob = Marshal.load(f) } else puts( "Saved data can't be found" ) end # b) Now transform ob in a singleton class << ob def xxx=( aStr ) @x = aStr end def xxx return @x end end
This code first checks whether a file containing the saved data can be found. (This sample has been kept deliberately simple—in a real application you would of course need to write some exception-handling code to deal with the possibility of reading in invalid data.) If the file is found, the data is loaded into an object of the generic X
type.
Only when this has been done is this object “transformed” into a singleton in the usual way. In other words, the object is loaded, and then the code beginning class << ob
executes (simply because the singleton-creation code occurs after the loading code and so is executed in sequence by the Ruby interpreter). This provides the object with the additional xxx
singleton method. You can then save the new data back to disk and reload and re-create the modified singleton, as explained earlier, at a later stage:
if ob.xxx == "hello" then ob.xxx = "goodbye" else ob.xxx = "hello" end File.open( FILENAME, 'w' ){ |f| Marshal.dump( ob, f ) }
If you wanted to save and load singletons in a real application, the singleton “reconstruction” code could, naturally, be given its own method so that you don’t have to rely upon its position in your code as in the previous example.
singleton_m4.rb
def makeIntoSingleton( someOb ) class << someOb def xxx=( aStr ) @x = aStr end def xxx return @x end end return someOb end