Reputation: 1221
Need to convert the following code from Ruby to C#. However I'm kind of puzzled by the use of the yield keyword and the general syntax of Ruby. Can anyone that knows a little bit Ruby please help out and convert the code
class < < Cache
STALE_REFRESH = 1
STALE_CREATED = 2
# Caches data received from a block
#
# The difference between this method and usual Cache.get
# is following: this method caches data and allows user
# to re-generate data when it is expired w/o running
# data generation code more than once so dog-pile effect
# won't bring our servers down
#
def smart_get(key, ttl = nil, generation_time = 30.seconds)
# Fallback to default caching approach if no ttl given
return get(key) { yield } unless ttl
# Create window for data refresh
real_ttl = ttl + generation_time * 2
stale_key = "#{key}.stale"
# Try to get data from memcache
value = get(key)
stale = get(stale_key)
# If stale key has expired, it is time to re-generate our data
unless stale
put(stale_key, STALE_REFRESH, generation_time) # lock
value = nil # force data re-generation
end
# If no data retrieved or data re-generation forced, re-generate data and reset stale key
unless value
value = yield
put(key, value, real_ttl)
put(stale_key, STALE_CREATED, ttl) # unlock
end
return value
end
end
Upvotes: 7
Views: 19761
Reputation: 369536
I don't know C# at all, so anything I say about C# should be taken with a grain of salt. However, I will try to explain what goes on in that piece of Ruby code.
class << Cache
Ruby has something called singleton methods. These have nothing to do with the Singleton Software Design Pattern, they are just methods that are defined for one and only one object. So, you can have two instances of the same class, and add methods to one of those two objects.
There are two different syntaxes for singleton methods. One is to just prefix the name of the method with the object, so def foo.bar(baz)
would define a method bar
only for object foo
. The other method is called opening up the singleton class and it looks syntactically similar to defining a class, because that's also what happens semantically: singleton methods actually live in an invisible class that gets inserted between the object and its actual class in the class hierarchy.
This syntax looks like this: class << foo
. This opens up the singleton class of object foo
and every method defined inside of that class body becomes a singleton method of object foo
.
Why is this used here? Well, Ruby is a pure object-oriented language, which means that everything, including classes is an object. Now, if methods can be added to individual objects, and classes are objects, this means that methods can be added to individual classes. In other words, Ruby has no need for the artificial distinction between regular methods and static methods (which are a fraud, anyway: they aren't really methods, just glorified procedures). What is a static method in C#, is just a regular method on a class object's singleton class.
All of this is just a longwinded way of explaining that everything defined between class << Cache
and its corresponding end
becomes static
.
STALE_REFRESH = 1
STALE_CREATED = 2
In Ruby, every variable that starts with a capital letter, is actually a constant. However, in this case we won't translate these as static const
fields, but rather an enum
, because that's how they are used.
# Caches data received from a block
#
# The difference between this method and usual Cache.get
# is following: this method caches data and allows user
# to re-generate data when it is expired w/o running
# data generation code more than once so dog-pile effect
# won't bring our servers down
#
def smart_get(key, ttl = nil, generation_time = 30.seconds)
This method has three parameters (four actually, we will see exactly why later), two of them are optional (ttl
and generation_time
). Both of them have a default value, however, in the case of ttl
the default value isn't really used, it serves more as a marker to find out whether the argument was passed in or not.
30.seconds
is an extension that the ActiveSupport
library adds to the Integer
class. It doesn't actually do anything, it just returns self
. It is used in this case just to make the method definition more readable. (There are other methods which do something more useful, e.g. Integer#minutes
, which returns self * 60
and Integer#hours
and so on.) We will use this as an indication, that the type of the parameter should not be int
but rather System.TimeSpan
.
# Fallback to default caching approach if no ttl given
return get(key) { yield } unless ttl
This contains several complex Ruby constructs. Let's start with the easiest one: trailing conditional modifiers. If a conditional body contains only one expression, then the conditional can be appended to the end of the expression. So, instead of saying if a > b then foo end
you can also say foo if a > b
. So, the above is equivalent to unless ttl then return get(key) { yield } end
.
The next one is also easy: unless
is just syntactic sugar for if not
. So, we are now at if not ttl then return get(key) { yield } end
Third is Ruby's truth system. In Ruby, truth is pretty simple. Actually, falseness is pretty simple, and truth falls out naturally: the special keyword false
is false, and the special keyword nil
is false, everything else is true. So, in this case the conditional will only be true, if ttl
is either false
or nil
. false
isn't a terrible sensible value for a timespan, so the only interesting one is nil
. The snippet would have been more clearly written like this: if ttl.nil? then return get(key) { yield } end
. Since the default value for the ttl
parameter is nil
, this conditional is true, if no argument was passed in for ttl
. So, the conditional is used to figure out with how many arguments the method was called, which means that we are not going to translate it as a conditional but rather as a method overload.
Now, on to the yield
. In Ruby, every method can accept an implicit code block as an argument. That's why I wrote above that the method actually takes four arguments, not three. A code block is just an anonymous piece of code that can be passed around, stored in a variable, and invoked later on. Ruby inherits blocks from Smalltalk, but the concept dates all the way back to 1958, to Lisp's lambda expressions. At the mention of anonymous code blocks, but at the very least now, at the mention of lambda expressions, you should know how to represent this implicit fourth method parameter: a delegate type, more specifically, a Func
.
So, what's yield
do? It transfers control to the block. It's basically just a very convenient way of invoking a block, without having to explicitly store it in a variable and then calling it.
# Create window for data refresh
real_ttl = ttl + generation_time * 2
stale_key = "#{key}.stale"
This #{foo}
syntax is called string interpolation. It means "replace the token inside the string with whatever the result of evaluating the expression between the braces". It's just a very concise version of String.Format()
, which is exactly what we are going to translate it to.
# Try to get data from memcache
value = get(key)
stale = get(stale_key)
# If stale key has expired, it is time to re-generate our data
unless stale
put(stale_key, STALE_REFRESH, generation_time) # lock
value = nil # force data re-generation
end
# If no data retrieved or data re-generation forced, re-generate data and reset stale key
unless value
value = yield
put(key, value, real_ttl)
put(stale_key, STALE_CREATED, ttl) # unlock
end
return value
end
end
This is my feeble attempt at translating the Ruby version to C#:
public class Cache<Tkey, Tvalue> {
enum Stale { Refresh, Created }
/* Caches data received from a delegate
*
* The difference between this method and usual Cache.get
* is following: this method caches data and allows user
* to re-generate data when it is expired w/o running
* data generation code more than once so dog-pile effect
* won't bring our servers down
*/
public static Tvalue SmartGet(Tkey key, TimeSpan ttl, TimeSpan generationTime, Func<Tvalue> strategy)
{
// Create window for data refresh
var realTtl = ttl + generationTime * 2;
var staleKey = String.Format("{0}stale", key);
// Try to get data from memcache
var value = Get(key);
var stale = Get(staleKey);
// If stale key has expired, it is time to re-generate our data
if (stale == null)
{
Put(staleKey, Stale.Refresh, generationTime); // lock
value = null; // force data re-generation
}
// If no data retrieved or data re-generation forced, re-generate data and reset stale key
if (value == null)
{
value = strategy();
Put(key, value, realTtl);
Put(staleKey, Stale.Created, ttl) // unlock
}
return value;
}
// Fallback to default caching approach if no ttl given
public static Tvalue SmartGet(Tkey key, Func<Tvalue> strategy) =>
Get(key, strategy);
// Simulate default argument for generationTime
// C# 4.0 has default arguments, so this wouldn't be needed.
public static Tvalue SmartGet(Tkey key, TimeSpan ttl, Func<Tvalue> strategy) =>
SmartGet(key, ttl, new TimeSpan(0, 0, 30), strategy);
// Convenience overloads to allow calling it the same way as
// in Ruby, by just passing in the timespans as integers in
// seconds.
public static Tvalue SmartGet(Tkey key, int ttl, int generationTime, Func<Tvalue> strategy) =>
SmartGet(key, new TimeSpan(0, 0, ttl), new TimeSpan(0, 0, generationTime), strategy);
public static Tvalue SmartGet(Tkey key, int ttl, Func<Tvalue> strategy) =>
SmartGet(key, new TimeSpan(0, 0, ttl), strategy);
}
Please note that I do not know C#, I do not know .NET, I have not tested this, I don't even know if it is syntactically valid. Hope it helps anyway.
Upvotes: 14
Reputation: 1
Try this:
def foo
if block.arity == 0
# No arguments passed, load defaults from config file and ignore call
else
yield
end
end
Upvotes: 0
Reputation: 6152
It looks like you're trying to port memcache-client from Ruby to C#. If so, it might be easier to use a native C# memcache client implementation such as:
http://code.google.com/p/beitmemcached/
Either way, I generally agree with MarkusQ that translating from a higher-level language to a lower-level language is probably going to be less productive overall than just rewriting in an idiomatic fashion for the target language. A straight translation from Ruby to C# is going to give you some very ugly code, at best.
The yield Ruby keyword allows you to invoke a block of code that was passed as an implicitly declared argument to the method. So, for example, you can think of the smart_get method definition as actually looking more like:
def smart_get(key, ttl = nil, generation_time = 30.seconds, &block)
And when you call smart_get as such:
x = smart_get("mykey", my_ttl) { do_some_operation_here }
The stuff in the braces gets assigned to the variable block in the expanded definition above. yield then invokes the code in &block. This is a gross simplification, but should help you get the general idea.
Back to your conversion. The simplification I just did isn't necessarily going to get you 100% there, because as soon as you find a C# way to translate that code, another piece of code will break your translation. For instance, let's say a given method needs to inspect the block:
def foo
if block.arity == 0
# No arguments passed, load defaults from config file and ignore call
else
yield
end
end
And of course since lambdas are first-class objects in Ruby, you may encounter code like the following:
foo = lambda { |a, b, c| a + b + c }
# foo is now defined as a function that sums its three arguments
And then God help you if you encounter code that defines methods on the fly, or (worse fo a translator) takes advantages of Ruby's malleable classes:
class Foo
def show
puts "Foo"
end
end
foo = Foo.new
foo.show # prints "Foo"
class <<foo; def show; puts "Bar"; end; end
foo.show # prints "Bar"
Foo.new.show # prints "Foo"
foo.show # Still prints "Bar"
Since each instance of each class in Ruby can have its own class definition, I'm not really sure how you would port even simple examples to C# without fancy gymnastics. Ruby has a lot of these features that will break a simple translation effort, so I would recommend learning the thing you're trying to port, and then redoing it from scratch.
Upvotes: 4
Reputation: 21950
It appears this code is being passed a block to be evaluated if the cache does not contain the requested data (yield
is how you call the block). This is fairly idiomatic ruby code; I don't know how (or even if) you could "translate it" to c#.
Look for a use case to see what I mean. You should find something vaguely like this:
x = smart_get([:foo,"bar"]) { call_expensive_operation_foo("bar") }
A better bet would be to figure out what you need it to do and write something that does that de novo in c#, rather than trying to "translate" from ruby.
Upvotes: 5