I need to customize what happens to one of my class on Marshal.load(). I found that there are marshal_load and _load methods but so far I was unsuccessful to use either of them to what I need.
It's crucial for me that the resulting binary data are not changed compared to if those methods are not defined. This rules out _dump and _load as they're for defining custom serialization string format.
I tried to use marshal_load and marshal_dump but it changes the resulting binary format as well. This can't happen.
EDIT: To be a bit more specific about my usecase I have a marshalled binary file which contains strings in 8bit. I need to change these strings to utf8 in marshal_load and back to 8bit in marshal_dump.
I created this repository for testing, read the comments here:
https://github.com/enumag/marshal-test/blob/master/test.rb
I saw in your Github example you have the below comment, but I don't think you have the option not to do it manually before the dump and after the load.
in practice I'm Marshal loading a large structure with many nested objects so I can't do it manually
I would suggest one of the two following schemes.
Create wrapper around Marshal and use that to modify your object before and after marshaling.
class Foo
def initialize(attr_1, attr_2)
#attr_1 = attr_1
#attr_2 = attr_2
end
end
class MyMarshaler
def load(file_name)
source = File.new(file_name, "r")
loaded = Marshal.load(source)
source.close
# do whatever you want with loaded object
manipulated = loaded
end
def dump(obj, file_name)
dest = File.new(file_name, "w")
# do whatever you want with object before dumping it
manipulated = obj
Marshal.dump(manipulated, dest)
dest.close
end
end
foo = Foo.new("foo-thing-1", "foo-thing-2")
foo_file_name = "marshaled.ruby_object"
marshaler = MyMarshaler.new
p foo
marshaler.dump(foo, foo_file_name)
restored_foo = marshaler.load(foo_file_name)
p restored_foo
The other option would be to create a Module and extend your class to include methods that wrap the marshal load/dump methods.
module MyMarshalerModule
def load(file_name)
source = File.new(file_name, "r")
loaded = Marshal.load(source)
source.close
# do whatever you want with loaded object
manipulated = loaded
end
def dump(obj, file_name)
dest = File.new(file_name, "w")
# do whatever you want with object before dumping it
manipulated = obj
Marshal.dump(manipulated, dest)
dest.close
end
end
class Bar
extend MyMarshalerModule
def initialize(attr_1, attr_2)
#attr_1 = attr_1
#attr_2 = attr_2
end
end
bar = Bar.new("bar-thing-1", "bar-thing-2")
bar_file_name = "marshaled2.ruby_object"
p bar
Bar.dump(bar, bar_file_name)
restored_bar = Bar.load(bar_file_name)
p restored_bar
I think my original answer addressed the question in your post, but in the comments you imply that you really have 2 questions.
This answer attempts to answer both questions.
Here is a TransMarshal class that allows you to pass a block to both the load and dump methods that recursively looks for instances of a class and transform those instances using a nested block. The objects of the transformation are untouched.
class TransMarshal
def self.load(file_name)
source = File.new(file_name, "r")
loaded = Marshal.load(source)
source.close
block_given? ? yield(loaded) : loaded
end
def self.dump(obj, file_name)
dest = File.new(file_name, "w")
manipulated = block_given? ? yield(obj) : obj
Marshal.dump(manipulated, dest)
dest.close
end
def self.transform(obj, klass, &block)
cloned_obj = obj.clone
return cloned_obj if obj.instance_variables.empty?
cloned_obj.instance_variables.each do |o|
cloned_inst_var = cloned_obj.instance_variable_get(o).clone
cloned_obj.instance_variable_set(o, transform(cloned_inst_var, klass, &block))
if cloned_inst_var.class == klass
cloned_obj.instance_variable_set(o, yield(cloned_inst_var))
end
end
cloned_obj
end
end
Example usage.
class Foo
def initialize(attr_1, attr_2)
#attr_1 = attr_1
#attr_2 = attr_2
end
end
class Bar
def initialize(attr_1, attr_2)
#attr_1 = attr_1
#attr_2 = attr_2
end
end
class FooBar
# extend MyMarshalerModule
def initialize(foo, bar, string, number)
#foo = foo
#bar = bar
#string = string
#number = number
end
end
foo = Foo.new("foo_attr_1", "foo_attr_2")
bar = Bar.new("bar_attr_1", "bar_attr_2")
foo_bar = FooBar.new(foo, bar, "foobar", 42)
TransMarshal.dump(foo_bar, "dumped") do |obj|
TransMarshal.transform(obj, String) {|s| "#{s}-x"}
end
raw_restored_foo_bar = TransMarshal.load("dumped")
puts "raw_restored_foo_bar = #{raw_restored_foo_bar.inspect}"
transformed_restored_foo_bar = TransMarshal.load("dumped") do |obj|
TransMarshal.transform(obj, String) { |o| o[0..-3] }
end
puts "transformed_restored_foo_bar = #{transformed_restored_foo_bar.inspect}"
Related
In Ruby, say I have a class called Song. I would like to know how correcty to write the initialize so that it supports 2 different types of. For example:
song_full = Song.new(fromSomeCloudStorage)
song_preview = Song.new(fromLocalStorage)
Then say I have a Song class, where I always want to assign the #time_stamp, but then depending on whether there is cloud_storage or not, assign #cloud_store_spec
def initialize(cloud_storage = nil, time_stamp = nil, local_storage = nil)
#time_stamp = time_stamp || (Time.now.to_f * 1000).to_i.to_s
#cloud_store_spec = cloud_storage
end
I'm thinking of using nil as I have done, however would the code know which is cloud_storage and which is local_storage. Is it actually possible?
Any help appreciated?
First off, use keyword arguments to allow you to pass the relevant data to the initializer (up to you if you want to include time_stamp as a keyword or regular arg):
def initialize(cloud_storage: nil, local_storage: nil, time_stamp: nil)
#time_stamp = time_stamp || (Time.now.to_f * 1000).to_i.to_s
#cloud_store_spec = cloud_storage
end
This will make things a lot clearer when calling the class, allowing:
song_full = Song.new(cloud_storage: fromSomeCloudStorage)
song_preview = Song.new(local_storage: fromLocalStorage)
In terms of how your code will know whether the code is from cloud or local, if you mean the class's instances, you can just check for the presence of #cloud_store_spec, something like:
def cloud_storage?
#cloud_store_spec.present?
end
Then, from anywhere else in your code, you can call:
song_full = Song.new(cloud_storage: fromSomeCloudStorage)
song_full.cloud_storage? # => true
song_preview = Song.new(local_storage: fromLocalStorage)
song_preview.cloud_storage? # => false
Hope that helps and I'm reading you right :) Let me know how you get on or if you've any questions.
Update for Ruby 1.9.3
As keyword arguments were introduced in Ruby 2.0, for 1.9.3 you can use an options hash:
def initialize(options = {}) # again, timestamp can be a separate arg if you'd prefer
#time_stamp = options[:time_stamp] || (Time.now.to_f * 1000).to_i.to_s
#cloud_store_spec = options[:cloud_storage]
end
The rest of the code will remain the same.
If CloudStorage or LocalStorage can be considered as classes:
class LocalStorage
def initialize; p 'LocalStorage class initialized'; end
end
class CloudStorage
def initialize; p 'CloudStorage class initialized'; end
def play; p "...playing song"; end
end
Maybe you could consider to hard code a Hash, (let apart time_stamp):
class Song
attr_reader :store_spec
def initialize(kind = nil)
storage_kind = {cloud: CloudStorage, preview: LocalStorage}
#store_spec = storage_kind[kind].new
end
def storage_kind
#store_spec.class
end
def play
#store_spec.play
end
end
So you can call for example:
song = Song.new(:cloud) #=> "CloudStorage class initialized"
song.storage_kind #=> CloudStorage
song.play #=> "...playing song"
I'm fairly new to ruby and wanted to know if it's possible to export a method/method call.
Basically, I have this:
class B
def initialize
#bptimer = BP_Timer.new("MyName", method(:sayhello))
end
def sayhello()
msgbox("hello")
end
end
class BP_Timer
def initialize(name = nil, method = nil)
#name = name
#time = 0
#repeats = 0
#start_from = 0
#method = method
#active = false
end
def start()
#active = true
$timers.push(self)
end
end
(I removed all the rather uninteresting parts of the code)
Basically, I now have a "BP_Timer" object with the name and method that will later be pushed into a $timers array which I want to save to a file and later on load from when I restart the program. Is it possible to deconstruct the object in such a way that I can store and restore it?
This is not how we usually store/restore the state. The common approach would be to store a configuration and perform the whole initialization again.
That said, you are to store an array of initialization parameters, like ["MyName", :sayhello] in your case to _YAML,JSON`, or any other text format of your choice:
config.yml
bp_timers:
- "MyName": :sayhello
And then on program start you do:
config = YAML.load_file("config.yml")
config["bp_timers"].each do |k, v|
BP_Timer.new(*[k, method(v)]).start
end
Answering your initial question: yes, this is possible to some extent with Marshal class.
I was working on a homework assignment when I ran into a frustrating issue. The assignment is an exercise in Ruby metaprogramming and the goal is to define an 'attr_accessor_with_history' that does all the same things as 'attr_accessor', but also provides a history of all values that an attribute has ever been. Here is the provided code from the assignment along with some code I added in an attempt to complete the assignment:
class Class
def attr_accessor_with_history(attr_name)
attr_name = attr_name.to_s
attr_hist_name = attr_name+'_history'
history_hash = {attr_name => []}
#getter
self.class_eval("def #{attr_name} ; ##{attr_name} ; end")
#setter
self.class_eval %Q{
def #{attr_name}=(val)
# add to history
##{attr_hist_name} = [nil] if ##{attr_hist_name}.nil?
##{attr_hist_name} << val
history_hash[##{attr_name}] = ##{attr_hist_name}
# set the value itself
##{attr_name} = val
end
def history(attr) ; #history_hash[attr.to_s] ; end
}
end
end
class Foo
attr_accessor_with_history :bar
attr_accessor_with_history :crud
end
f = Foo.new # => #<Foo:0x127e678>
f.bar = 3 # => 3
f.bar = :wowzo # => :wowzo
f.bar = 'boo!' # => 'boo!'
puts f.history(:bar) # => [3, :wowzo, 'boo!']
f.crud = 42
f.crud = "Hello World!"
puts f.history(:crud)
I wanted to use a hash to store different histories for different attributes but I cannot access that hash in the class_eval statement for the setter. No matter how I try to set it up I always either seem to get a NoMethodError for the []= method because 'history_hash' somehow becomes type NilClass, or a NameError occurs because it sees 'history_hash' as an undefined local variable or method. How do I use the hash in the class_eval statements?
or a NameError occurs because it sees 'history_hash' as an undefined local variable or method
I'd say you can't, because it is a local variable, one that is inaccessible in the context you want it. However, why do you even need it? I'm reasonably sure it's in the "some code I added in an attempt to complete the assignment", and not the original assignment code (which, I assume, expects you to store the history of #bar in #bar_history - or else what is attr_hist_name all about?)
I'm also uncomfortable about string evals; it's generally not necessary, and Ruby can do better, with its powerful metaprogramming facilities. Here's how I'd do it:
class Class
def attr_accessor_with_history(attr_name)
attr_setter_name = :"#{attr_name}="
attr_getter_name = :"#{attr_name}"
attr_hist_name = :"##{attr_name}_history"
attr_name = :"##{attr_name}"
self.class_eval do
define_method(attr_getter_name) do
instance_variable_get(attr_name)
end
define_method(attr_setter_name) do |val|
instance_variable_set(attr_name, val)
history = instance_variable_get(attr_hist_name)
instance_variable_set(attr_hist_name, history = []) unless history
history << val
end
end
end
end
class Object
def history(attr_name)
attr_hist_name = :"##{attr_name}_history"
instance_variable_get(attr_hist_name)
end
end
Finally, as it's monkey-patching base classes, I'd rather use refinements to add it where needed, but that's probably an overkill for an assignment.
I just started with ruby, and just started learning oop today, after making a class, I am trying to print to console yet I keep getting this error. Does anyone know what's wrong?
undefined method `set_brand_name=' for # (NoMethodError)
Here is the code causing this error:
class Laptop
def set_brand_name(brand_name)
#brand = brand_name
end
def get_brand_name
return #brand
end
def set_color(color)
#color = color
end
def get_color
return #color
end
def set_processor(processor)
#processor = processor
end
def get_processor
return #processor
end
def set_storage(hard_drive)
#storage = hard_drive
end
def get_storage
return #storage
end
def set_memory(ram)
#memory = ram
end
def get_memory
return #memory
end
end
my_laptop = Laptop.new
my_laptop.set_brand_name = "HP"
my_laptop.set_processor = 'i7-4700k'
my_laptop.set_memory = '16gb'
my_laptop.set_storage = '750gb'
my_laptop.set_color = 'Silver'
brand = my_laptop.get_brand_name
color = my_laptop.get_color
processor = my_laptop.get_processor
memory = my_laptop.get_memory
storage = my_laptop.get_storage
This should output the message:
"""The Laptop I want is an #{brand}, it has a #{processor},
#{memory} of ram, a #{storage}, and it #{color}!!!"""
What am I doing wrong?
The problem is that you are not calling the method names as you've defined them. You defined set_brand_name without an equal sign so use:
my_laptop.set_brand_name("HP")
I would simply the getters and setters like so:
class Laptop
def brand_name=(brand_name)
#brand_name = brand_name
end
def brand_name
#brand_name
end
end
Or even better:
class Laptop
attr_accessor :brand_name
end
Then you can use it the same way:
my_laptop = Laptop.new
my_laptop.brand_name = "HP"
puts my_laptop.brand_name # => "HP"
In line 45, you are calling the method set_brand_name=, but your Laptop class doesn't have a method with that name. You need to either call the method which you do have (set_brand_name), or rename the set_brand_name method to set_brand_name=.
Note that neither of those two is idiomatic, though. Idiomatically, the method should be named brand_name= (without the set_ prefix, the "setting" part is already implied by the = sign), and you shouldn't define it manually, but programmatically using the Module#attr_writer method.
Your entire code can be condensed to:
Laptop = Struct.new(:brand_name, :color, :processor, :storage, :memory)
my_laptop = Laptop.new('HP', 'Silver', 'i7-4700k', '750gb', '16gb')
brand = my_laptop.brand_name
color = my_laptop.color
processor = my_laptop.processor
memory = my_laptop.memory
storage = my_laptop.storage
puts "The Laptop I want is an #{brand}, it has a #{processor}, #{memory} of ram, a #{storage}, and it's #{color}!!!"
Your setter methods are defined incorrectly.
Here's your definition of the set_brand_name method:
def set_brand_name(brand_name)
#brand = brand_name
end
And here's how you're calling it:
my_laptop.set_brand_name = "HP"
You're calling the method incorrectly. If you'd like to keep your definition, you should be calling it like this:
my_laptop.set_brand_name("HP")
Or, if you'd like to use the equals sign, you should define your method like this:
def set_brand_name=(brand_name)
#brand = brand_name
end
Notice the equals in the method definition? You're required to use it when you want the setter to look like a regular assignment.
However, for most trivial cases you don't need to define getters and setters manually. You can just use attr_accessor on the class and pass it the properties you want to define. Here's what your class would look like with attr_accessor:
class Laptop
attr_accessor: :brand_name, :color, :processor, :storage, :memory
end
my_laptop = Laptop.new
my_laptop.brand_name = "HP"
my_laptop.processor = 'i7-4700k'
my_laptop.memory = '16gb'
my_laptop.storage = '750gb'
my_laptop.color = 'Silver'
brand = my_laptop.brand_name
color = my_laptop.color
processor = my_laptop.processor
memory = my_laptop.memory
storage = my_laptop.storage
puts """The Laptop I want is an #{brand}, it has a #{processor},
#{memory} of ram, a #{storage}, and it #{color}!!!"""
I encourage you to try it.
Let's say I have a class Foo and the constructor takes 2 parameters.
Based on these parameters the initialize method does some heavy calculations and stores them as variables in the instance of the class. Object created.
Now I want to optimize this and create a cache of these objects. When creating a new Foo object, I want to return a existing one from the cache if the parameters match. How can I do this?
I currently have a self.new_using_cache(param1, param2), but I would love to have this integrated in the normal Foo.new().
Is this possible in any way?
I can also deduct that using .new() combined with a cache is not really syntactical correct.
That would mean that the method should be called new_or_from_cache().
clarification
It's not just about the heavy calculation, it's also preferred because of limiting the amount of duplicate objects. I don't want 5000 objects in memory, when I can have 50 unique ones from a cache. So I really need to customize the .new method, not just the cached values.
class Foo
##cache = {}
def self.new(value)
if ##cache[value]
##cache[value]
else
##cache[value] = super(value)
end
end
def initialize(value)
#value = value
end
end
puts Foo.new(1).object_id #2148123860
puts Foo.new(2).object_id #2148123820 (different from first instance)
puts Foo.new(1).object_id #2148123860 (same as first instance)
You can actually define self.new, then call super if you actually want to use Class#new.
Also, this totally approach prevents any instantiation from ever occurring if a new instance isn't actually needed. This is die to the fact the initialize method doesn't actually make the decision.
Here's a solution I came up with by defining a generic caching module. The module expects your class to implement the "retrieve_from_cache" and "store_in_cache" methods. If those methods don't exist, it doesn't attempt to do any fancy caching.
module CacheInitializer
def new(*args)
if respond_to?(:retrieve_from_cache) &&
cache_hit = retrieve_from_cache(*args)
cache_hit
else
object = super
store_in_cache(object, *args) if respond_to?(:store_in_cache)
object
end
end
end
class MyObject
attr_accessor :foo, :bar
extend CacheInitializer
#cache = {}
def initialize(foo, bar)
#foo = foo
#bar = bar
end
def self.retrieve_from_cache(foo, bar)
# grab the object from the cache
#cache[cache_key(foo, bar)]
end
def self.store_in_cache(object, foo, bar)
# write back to cache
#cache[cache_key(foo, bar)] = object
end
private
def self.cache_key(foo, bar)
foo + bar
end
end
Something like this?
class Foo
##cache = {}
def initialize prm1, prm2
if ##cache.key?([prm1, prm2]) then #prm1, #prm2 = ##cache[[prm1, prm2]] else
#prm1 = ...
#prm2 = ...
##cache[[prm1, prm2]] = [#prm1, #prm2]
end
end
end
Edited
To not create an instance when the parameters are the same as before,
class Foo
##cache = {}
def self.new prm1, prm2
return if ##cache.key?([prm1, prm2])
#prm1 = ...
#prm2 = ...
##cache[[prm1, prm2]] = [#prm1, #prm2]
super
end
end
p Foo.new(1, 2)
p Foo.new(3, 4)
p Foo.new(1, 2)
# => #<Foo:0x897c4f0>
# => #<Foo:0x897c478>
# => nil
You could use a class-level instance variable to store results from previous object instantiations:
class Foo
#object_cache = {}
def initialize(param1, param2)
#foo1 = #object_cache[param1] || #object_cache[param1] = expensive_calculation
#foo2 = #object_cache[param2] || #object_cache[param2] = expensive_calculation
end
private
def expensive_calculation
...
enf
end
As you probably know you have reinvented the factory method design pattern and it's a perfectly valid solution using your name for the factory method. In fact, it's probably better to do it without redefining new if anyone else is going to have to understand it.
But, it can be done. Here is my take:
class Test
##cache = {}
class << self
alias_method :real_new, :new
end
def self.new p1
o = ##cache[p1]
if o
s = "returning cached object"
else
##cache[p1] = o = real_new(p1)
s = "created new object"
end
puts "%s (%d: %x)" % [s, p1, o.object_id]
o
end
def initialize p
puts "(initialize #{p})"
end
end
Test.new 1
Test.new 2
Test.new 1
Test.new 2
Test.new 3
And this results in:
(initialize 1)
created new object (1: 81176de0)
(initialize 2)
created new object (2: 81176d54)
returning cached object (1: 81176de0)
returning cached object (2: 81176d54)
(initialize 3)