I've got a little DSL that looks like this:
ActivityLogger.log do
activity('27-06-2012') do
eat do |act|
act.duration = 15
act.priority = 5
end
end
end
I want to refactor it so it loses the block params in the innermost block, so it looks like this:
ActivityLogger.log do
activity('27-06-2012') do
eat do
duration = 15
priority = 5
end
end
end
The #eat method instantiates a Log object:
def eat(&block)
#logs << Log.new(Eat, &block)
end
Log's constructor yields self in the last line:
def initialize(activity, &block)
#activity = activity
yield self
end
To my mind, that is where the problem is. I've tried both using instance_eval in the #eat method (see link#2 below) and removing the yield statement from the Log's constructor entirely (link#3), but none of these approaches work (the Log object gets created, but doesn't get its #duration and #priority methods set).
Here are the links:
1) A working DSL with block parameters
2) Non-working DSL, first refactoring attempt
3) Non-working DSL, second refactoring attempt
Thanks!
duration = 15 doesn't call the setter method as you expect but defines a local variable duration. You have to either call the setter explicitly via self.duration = 15 or implement your setter like
def duration(value)
#duration = value
end
and call duration 15.
Related
This is a job interview problem. I'm supposed to create a data structure for a time in seconds and milliseconds, then create two Time objects, and then write a function that can return the difference between the two Times. This is my code:
class Time
def initialize (sec, milli_sec)
#sec = sec
#milli_sec = milli_sec
end
def difference(time_2)
puts #sec.to_i*1000 + #milli_sec.to_i + time_2.#sec
end
end
time_1 = Time.new('5','30')
time_2 = Time.new('6','40')
time_1.difference(time_2)
This is the error:
syntax error, unexpected tIVAR, expecting '('
I am having a problem accessing the #sec, #milli_sec variables of time_2 passed as time_1.difference(time_2). I think that the syntax is time_2.#sec.to_i or time_2.##sec.to_i, but those return errors. time_2.sec returns uninitialized time, even though it looks like it's been initialized. I would like to know the solution to this problem.
#sec and #milli_sec are instance variables of your Time class. This means that unless you do something else only the instance itself has access to them. Other parts of your code can create an instance, but are only able to access the methods you specify. The point of this is so that you can change the underlying implementation without affecting other parts of your code that are using this class.
There are a variety of ways to do this. You could define the following two methods in your Time class:
def sec
#sec
end
def milli_sec
#milli_sec
end
These methods are public (by default) so you can now do this:
t = Time.new(1, 2)
puts t.sec # prints 1
puts t.milli_sec # prints 2
A more Ruby-ish way would be to add this line to the top of your class:
attr_reader :sec, :milli_sec
Doing this accomplishes the same thing as defining the two methods above. You might also want to look at attr_accessor.
P.S. Time is a poor choice of class name for your own code as it's already defined by Ruby itself.
In ruby, every interface is a group of methods. You can't just obj.#var to access instance variables since they are not methods thus not interfaces. If you want to expose instance variables, then create public methods to access them. attr_reader and attr_accessor are simply handy ways to create those methods.
Here is my final solution:
class Moment
def initialize (sec, milli_sec)
#sec = sec
#milli_sec = milli_sec
end
def sec
#sec
end
def milli_sec
#milli_sec
end
def difference(time_2)
return ((#sec.to_i*1000 + #milli_sec.to_i) - (time_2.sec.to_i*1000 + time_2.milli_sec.to_i)).abs
end
end
time_1 = Moment.new('1','300')
time_2 = Moment.new('11','20')
time_1.difference(time_2)
The same objective can be achieved in much lesser lines of code using approach as below:-
class MyTime
attr_accessor :seconds, :milli_seconds
def initialize(entity={})
#seconds = entity['seconds']
#milli_seconds = entity['milli_seconds']
end
def difference(obj)
ms= ((#seconds.to_i*1000+#milli_seconds.to_i)-(obj.seconds.to_i*1000+obj.milli_seconds.to_i))
secs = ms/1000
msecs = ms%1000
return MyTime.new({'seconds'=> secs,
'milli_seconds'=> msecs})
end
end
time_1 = MyTime.new({'seconds'=> '20',
'milli_seconds'=> '100'})
time_2 = MyTime.new({'seconds'=> '10',`enter code here`
'milli_seconds'=> '20'})
diff_Obj = time_1.difference(time_2)
puts "Difference is : #{diff_Obj.seconds} Seconds #{diff_Obj.milli_seconds} milliseconds"
How can I create an opbjet that's totally lazy by itself? I have a block, and I want to pass around (as a dependency) the "current value" (at call time) of the block instead of the value at dependency injection time.
I can't actually pass around a lambda because all the services expect an actual object, so they won't send :call to them, just access them.
This (oversimplified) example might clarify the situation:
class Timer
def initialize(current_time)
#current_time = current_time
end
def print_current_time
print #current_time
end
end
class Injector
def current_time
# a lazy object that when accessed actually calls the lambda below
# every single time.
end
def current_time_lazy
-> { Time.now }
end
def instantiate(class_name)
# search for the class, look at the constructor and
# create an instance with the dependencies injected by
# name
# but to be simple
if class_name == "Timer"
Timer.new(current_time)
end
end
end
timer = Injector.new.instantiate("Timer")
timer.print_current_time # => some time
sleep 2
timer.print_current_time # => some *different* time
The actual situation implies passing around the current_user but depending on the situation the current user might change after those values are injected.
I would really appreciate any suggestion (even if for now I will carefully sort the dependency injection code so this doesn't happen, but I think it's pretty fragile)
This should help :
class Timer
def initialize(current_time)
#current_time = current_time
end
def print_current_time
puts #current_time
end
end
class LazyMaker < BasicObject
def self.instantiate(class_name, lambada)
if class_name == 'Timer'
::Timer.new(new(class_name, lambada))
end
end
def initialize(class_name, lambada)
#lambada = lambada
end
def method_missing(method, *args)
#lambada.call.send(method, *args)
end
end
timer = LazyMaker.instantiate('Timer', -> { Time.now })
timer.print_current_time # some time
sleep 2
timer.print_current_time # some other time
I'm trying to use delegation to implement it, so that I can call the block first, get a new object and redirect the method call to it. Why this way ? Because basically, accessing an object to do something means to call a method on it. For instance, in print #current_time, it sends #current_time.to_s.
But since almost all objects will have a few methods inherited from standard base classes in Ruby like Object, LazyMaker also has methods like to_s. So I thought of making just the LazyMaker inherit from BasicObject, which is a blank class. So almost all of the methods get delegated.
But yeah, there might be another way to do this.
I've been looking online, but I can't seem to crack this poxy Proxy koan!
Here's what I have as my Proxy class:
class Proxy
def initialize(target_object)
#object = target_object
# ADD MORE CODE HERE
#messages = []
end
# WRITE CODE HERE
def method_missing(method_name, *args)
if #object.respond_to?(method_name)
#messages << method_name
#object.__send__(method_name, *args)
end
end
end
Further down the code a Proxy Television gets instantiated and has its .channel set to 10, thus:
tv = Proxy.new(Television.new)
tv.channel = 10
I'm NOW getting the following error:
expected 10 to equal [:channel=, :power, :channel]
I have so many questions, I'm not sure where to start:
WHY does the method_missing method return an Array?
WHY does the first element in the Array end with a '='?
WHY, when I add...
def channel
#object.channel
end
...to the proxy, does the koans command line throw one of those elaborately drawn 'mountains are again merely mountains' errors?
And finally, can I quit now?
Any advice on these questions would be appreciated.
Don't quit! :)
I guess the main thing that you have to understand is the method_missing method. Within the if statement, the last line takes the method that the target object (in this case an instance of Television) is calling and saves it in an array called #messages. When you do tv.channel = 10, your target object is calling the channel= method.
Since that's the last thing in the method, method_missing returns that array.
The first item in the array is simply the "channel=" method, which is a method naming convention in ruby.
As for the last question, it'll throw an error because you're calling the method from within itself, which in theory will go on forever.
I hope that makes some sense.
I'm wondering what's the canonical way in Ruby to create custom setter and getter methods. Normally, I'd do this via attr_accessor but I'm in the context of creating a DSL. In a DSL, setters are called like this (using the = sign will create local variables):
work do
duration 15
priority 5
end
Therefore, they must be implemented like this:
def duration(dur)
#duration = dur
end
However this makes implementing a getter a bit tricky: creating a method with the same name but with no arguments will just overwrite the setter.
So I wrote custom methods that do both the setting and the getting:
def duration(dur=nil)
return #duration = dur if dur
return #duration if #duration
raise AttributeNotDefinedException, "Attribute '#{__method__}' hasn't been set"
end
Is this a good way to go about it? Here's a gist with test cases:
Ruby Custom Getters & Setters
Thanks!
The trickier case is if you want to set duration to nil. I can think of two ways of doing this
def duration(*args)
#duration = args.first unless args.empty?
#duration
end
Allow people to pass any number of args and decide what to do based on the number. You could also raise an exception if more than one argument is passed.
Another way is
def duration(value = (getter=true;nil))
#duration = value unless getter
#duration
end
This exploits default arguments a little: they can be pretty much any expression.
When called with no arguments getter is set to true, but when an argument is supplied (even if it is nil) the default value is not evaluated. Because of how local variable scope works getter ends up nil.
Possibly a little too clever, but the method body itself is cleaner.
For something like this, I prefer to separate the underlying class from the DSL. That is, make a Work class that has the usual accessors, duration and duration=. And to use that class via a DSL, wrap the work instance with something that can invoke the accessors situationally, like this:
class AccessorMultiplexer
def initialize(target)
#target = target
end
def method_missing(method, *args)
method = "#{method}=" unless args.empty?
#target.send method, *args
end
end
Wherever you want to use your Work class via a DSL, you'd wrap it with AccessorMultiplexer.new(work).
If you're opposed to metaprogramming in the wrapper, you could always make a specific WorkDSL wrapper that does the same without using method_missing. But it would maintain the separation and keep your Work class from being polluted by the quirks of the DSL syntax. You may want to use the Work class somewhere else in your code where the DSL would be in the way. In rake or a script or -- who knows.
(Adapted from my answer on codereview.)
That seems fine to me, although it seems strange that you raise an error if the value hasn't been set. This is what I would normally do:
def duration(dur=nil)
#duration = dur if dur
#duration
end
However this is a simplistic approach because it means you can't set #duration back to nil using just this method
I usually do this
def duration dur='UNDEFINED'
#duration = dur if dur != 'UNDEFINED'
#duration
end
You can replace UNDEFINED with your favorite thing
class MyClass
def test
...
end
end
tmp = MyClass.new
tmp.test do |t|
"here"
end
Why am I getting the error
multiple values for a block parameter (0 for 1)
Here is a slightly longer example, based on your code:
class MyClass
def test
yield self
end
def my_own_puts s
puts s
end
end
tmp = MyClass.new
tmp.test do |t|
t.my_own_puts "here"
end
Running this code will output "here".
What is happening is there is a method test that can take a block of code, so you can call it with the do .. end syntax. Because it is passing an arg to yield, that arg is available to the block, so you give this to the block using the do |some_arg_name| ... end syntax.
The yield is where the block gets executed in the test method, and in this case I to yield I pass in self. Since the block now has access to self (an instance of MyClass), the block can call the my_own_puts method on it, and print out "here".
if test is defined with a yield statement, when that statement is reached and if there is a parameter on the yield statement, that parameter will be put into the block variable t. Thus if you have:
def test
.....
yield x
......
end
then x will be the value of t when yield is executed.
With your help, I was able to get the code working like this
class MyClass
def test
a = yield self
puts a
end
end
tmp = MyClass.new
tmp.test do |t|
"here"
end
Thanks, I had to tweak your code a bit but it works the way I wanted to now.
Passing a block to a function (as Bob shows in his answer) is overkill in this case. If you are reading in a string and printing it out, all you should need is something like:
class MyClass
def test(a)
puts a
end
end
tmp = MyClass.new
tmp.test("here")
Using a block might function correctly, but you are calling a lot of unnecessary code and making the true nature of your code unclear.
Proper block usage aside, let me address the particular error message you are seeing. When you say tmp.test do |t|, Ruby is expecting tmp.test to yield a single value which it will temporarily call t and pass to the block (think like the block is a function and you are passing it the argument your yield statement as a parameter). In your case, the method test method must not be yield-ing anything, thus the message "(0 for 1)" implies that it is seeing zero objects yielded when it is expecting to see one. I don't know what your code for test does, but check and make sure that test yields exactly one value.