I know that in Ruby you can create hash maps like this:
hash = {"name"=>"John"}
But is it possible to have a hash map of methods:
hash = {"v"=> show_version}
and when calling hash["v"] either to execute the show_version function, or to return some kind on pointer that passed to some special method, to execute the function from the hash?
What I am trying to achieve, is to have a hash map of methods, instead of using a case/when construction, as it looks too verbose to me.
Not exactly like this, no. You need to get a hold of the Method proxy object for the method and store that in the Hash:
hash = { 'v' => method(:show_version) }
And you need to call the Method object:
hash['v'].()
Method duck-types Proc, so you could even store simple Procs alongside Methods in the Hash and there would be no need to distinguish between them, because both of them are called the same way:
hash['h'] = -> { puts 'Hello' }
hash['h'].()
# Hello
Related
I have two hashes as so:
deploy = {}
config[:mysql] = {}
I also have a function as so:
def some_cool_function(mysql_config)
{
foo:bar
}
end
When I call the function and assign the results to a hash as so:
deploy.mysql = some_cool_function(config.mysql)
I get NoMethodError: undefined method mysql for #<Hash:0x000055a0f2929f58>
It's possible the hash mysql just doesn't exist in the config hash, but i haven't investigated that option yet.
It looks right to me though, am I doing something incorrectly? I'm fairly new to the ruby language.
I'm trying to return a list of values based on user defined arguments, from hashes defined in the local environment.
def my_method *args
#initialize accumulator
accumulator = Hash.new(0)
#define hashes in local environment
foo=Hash["key1"=>["var1","var2"],"key2"=>["var3","var4","var5"]]
bar=Hash["key3"=>["var6"],"key4"=>["var7","var8","var9"],"key5"=>["var10","var11","var12"]]
baz=Hash["key6"=>["var13","var14","var15","var16"]]
#iterate over args and build accumulator
args.each do |x|
if foo.has_key?(x)
accumulator=foo.assoc(x)
elsif bar.has_key?(x)
accumulator=bar.assoc(x)
elsif baz.has_key?(x)
accumulator=baz.assoc(x)
else
puts "invalid input"
end
end
#convert accumulator to list, and return value
return accumulator = accumulator.to_a {|k,v| [k].product(v).flatten}
end
The user is to call the method with arguments that are keywords, and the function to return a list of values associated with each keyword received.
For instance
> my_method(key5,key6,key1)
=> ["var10","var11","var12","var13","var14","var15","var16","var1","var2"]
The output can be in any order. I received the following error when I tried to run the code:
undefined method `assoc' for #<Hash:0x10f591518> (NoMethodError)
Please would you point me how to troubleshoot this? In Terminal assoc performs exactly how I expect it to:
> foo.assoc("key1")
=> ["var1","var2"]
I'm guessing you're coming to Ruby from some other language, as there is a lot of unnecessary cruft in this method. Furthermore, it won't return what you expect for a variety of reasons.
`accumulator = Hash.new(0)`
This is unnecessary, as (1), you're expecting an array to be returned, and (2), you don't need to pre-initialize variables in ruby.
The Hash[...] syntax is unconventional in this context, and is typically used to convert some other enumerable (usually an array) into a hash, as in Hash[1,2,3,4] #=> { 1 => 2, 3 => 4}. When you're defining a hash, you can just use the curly brackets { ... }.
For every iteration of args, you're assigning accumulator to the result of the hash lookup instead of accumulating values (which, based on your example output, is what you need to do). Instead, you should be looking at various array concatenation methods like push, +=, <<, etc.
As it looks like you don't need the keys in the result, assoc is probably overkill. You would be better served with fetch or simple bracket lookup (hash[key]).
Finally, while you can call any method in Ruby with a block, as you've done with to_a, unless the method specifically yields a value to the block, Ruby will ignore it, so [k].product(v).flatten isn't actually doing anything.
I don't mean to be too critical - Ruby's syntax is extremely flexible but also relatively compact compared to other languages, which means it's easy to take it too far and end up with hard to understand and hard to maintain methods.
There is another side effect of how your method is constructed wherein the accumulator will only collect the values from the first hash that has a particular key, even if more than one hash has that key. Since I don't know if that's intentional or not, I'll preserve this functionality.
Here is a version of your method that returns what you expect:
def my_method(*args)
foo = { "key1"=>["var1","var2"],"key2"=>["var3","var4","var5"] }
bar = { "key3"=>["var6"],"key4"=>["var7","var8","var9"],"key5"=>["var10","var11","var12"] }
baz = { "key6"=>["var13","var14","var15","var16"] }
merged = [foo, bar, baz].reverse.inject({}, :merge)
args.inject([]) do |array, key|
array += Array(merged[key])
end
end
In general, I wouldn't define a method with built-in data, but I'm going to leave it in to be closer to your original method. Hash#merge combines two hashes and overwrites any duplicate keys in the original hash with those in the argument hash. The Array() call coerces an array even when the key is not present, so you don't need to explicitly handle that error.
I would encourage you to look up the inject method - it's quite versatile and is useful in many situations. inject uses its own accumulator variable (optionally defined as an argument) which is yielded to the block as the first block parameter.
I'm using a rake task to receive and handle data.
The data looks like "code:value" where each code maps to a specific action.
For example, "0xFE:0x47" calls the method corresponding to the 0xFE tag with the parameter 0x47.
For scalability purposes I think this should be mapped to an hash and have the methods defined below:
tags = Hash[0xFA => taskA, 0xFB => taskB, 0xFC => taskC]
def taskA(value)
...
end
def taskB(value)
...
end
def taskC(value)
...
end
then, when a message is received, do a split and call the method on the hash, like:
tokens = message.split(':')
tags[tokens[0]](tokens[1])
Ruby doesn't like the Hash initialization. What's the correct way to solve this problem?
Maybe you're expecting the methods to work like they do in JavaScript, where they're just references until called, but this is not the case. The best approach is to keep them as symbols and then use the send method to call them:
# Define a mapping table between token and method to call
tags = {
0xFA => :taskA,
0xFB => :taskB,
0xFC => :taskC
}
tokens = message.split(/:/)
# Call the method and pass through the value
send(tags[tokens[0]], tokens[1])
The Hash[] initializer is usually reserved for special cases, such as when converting an Array into a Hash. In this case it's redundant if not confusing so is best omitted. { ... } has the effect of creating a Hash implicitly.
Define "doesn't like".
If you try to initialize the hash using function names:
Before the function is defined, you'll get an undefined symbol.
After the function is defined, you're just executing the function.
If you want to map values to functions you can call, consider defining the methods as Procs/lambdas:
> taskA = lambda { |value| puts "foo #{value}" }
> h = { 0x42 => taskA }
> h[0x42].call("bar")
foo bar
You could store symbols, too, but I prefer to use known-existing artifacts, like a variable, so an IDE can help me make sure I'm doing things as right as it can know–symbols are arbitrary and there's no way to make sure they align with an existing method other than trying to call it.
I have a method which accepts an argument which can be an Array/Set-like object, or a Hash. The gist of the method is something like:
def find(query = {})
if Array === query or Set === query
query = {:_id => {'$in' => query.to_a}}
end
mongo_collection.find(query)
end
The method will accept a set of ID objects and turn it into a hash condition for MongoDB.
Two problems with above code:
It will fail if 'set' is not required from standard library. I don't want to require the dependency just to perform a check.
I don't want to do strict type comparisons. I want to accept any array- or set-like value and cast it to an array of values with to_a.
How would you perform this check? Some considerations to have in mind:
I could check for to_ary method, but Set doesn't respond to to_ary. Objects that implement this method should fundamentally be arrays, and I agree that Set isn't fundamentally an array. See Consequences of implementing to_int and to_str in Ruby
I can't check for to_a since Hash responds to it
Methods that are common to Array and Set, but not to Hash are:
[:&, :+, :-, :<<, :collect!, :flatten!, :map!, :|]
I decided to go with something like this:
query = {:_id => {'$in' => query.to_a}} if query.respond_to? :&
since intersection is likely an operator a set-like object would have. But I'm not sure about this.
Here's my take:
if not Hash === query and query.respond_to? :to_a
I'm just checking for to_a, which is the only method I'm interested in, but also ensuring that it's not a Hash object. I'm using strict type checking for Hash, but only because this is the least likely object to be passed as a completely separate class that's fundamentally a hash.
How about trying to find out if the query is Hash like?
def find(query = {})
query = {:_id => {'$in' => query.to_a}} unless query.respond_to?(:has_key?)
mongo_collection.find(query)
end
It is reasonable to expect that the object will be a Hash or Hash like if it responds to has_key?.
Checking to see if Set is defined would solve your first issue. For the second, you could possibly check the ancestors of the class of query to see if Array is in them, but that probably won't catch all "array-like" objects. I probably wouldn't check for the existence of methods to test for arrayness, as you are testing names, not behavior. Arel in particular responds to (or did before it was deprecated) &, but this type of object wouldn't work like you wanted it to.
Personally I'm thinking...
def find(query = {})
mongo_collection.find(query_formatter(query))
end
def query_formatter(query)
if query.respond_to?(:to_a) && !query.kind_of?(Hash)
{:_id => {'$in' => query.to_a}}
else
query
end
end
Given this code:
a = {1=>2}
m = a.method(:[])
I know that I can now use :
value = m.call(1)
and it will return 2. The thing is, what do I need to change so that I can call the method directly like :
m.call()
and it will get the 1 sent as a parameter? It would be nice to be able to write something like :
m = a.method(:[],1) # where the symbol is the method, and 1 will be the parameter it will be called with
The thing is, I'd like to delay the execution of certain parts of my script until some objects get created, and I'd like to avoid rewriting EVERYTHING to use lambdas.
Basically, what you want is a way to curry the function.
http://en.wikipedia.org/wiki/Curry_function
This can be done in many different ways, one of which:
def curry(method, *params)
lambda { send method, *params }
end
You can add this to Hash's metaclass, or to a module you want to include in some of your objects, etc. Then, calling it becomes the usecase you wanted:
irb(main):001:0> a = {1 => 2}
=> {1=>2}
... # add curry to Hash's metaclass
irb(main):011:0> m = a.curry :[], 1
=> #<Proc:0xb76e2154#(irb):8>
irb(main):012:0> m.call
=> 2
There's more than one way to do it, I'm sure.
a = {1=>2}
class << a
def fetch_what(key)
Proc.new { self[key] }
end
end
....
m = a.fetch_what(1)
m.call() # -> 2
It sounds like you should attach the method parameters to the object you're calling the method on, and have the method access them as instance variables.
In terms of simple refactoring steps:
Introduce new instance variables, one per method parameter.
Introduce new accessors for the instance variables.
Refactor the method to use the instance variables if the parameters are not supplied.
Refactor the calling code to set the instance variables through the accessors, at some point prior to the method call.
Refactor the calling code to pass no parameters in the method call.
As an example, refactor calling code like this:
widget = Widget.new
assembly_method = widget.method(:assemble)
# Time passes...
assembly_method.call(:electric, :smooth)
to work like this:
widget = Widget.new
widget.frombulator = :electric
widget.jazzifier = :smooth
assembly_method = widget.method(:assemble)
# Time passes...
assembly_method.call
It's not sexy or clever, but it will result in code that expresses its intent, and odds are good that it will address the real problem, namely that something is missing from your model.