shanemcd
shanemcd

Reputation: 578

Help explaining symbols as hash values in Ruby?

Total Ruby noob. I'm going through Zed Shaw's LRTHW, and I'm stuck on the Hash exercise. I can't make any sense of this code, and can't find anything on the web that looks like it.

cities = {'CA' => 'San Francisco', 'MI' => 'Detroit', 'FL' => 'Jacksonville'}

cities['NY'] = 'New York'
cities['OR'] = 'Portland'

def find_city (map, state)
  if map.include? state
    return map[state]
  else
    return "Not found."
  end
end

cities[:find] = method(:find_city)

while true
  print "State? (Enter to quit) "
  state = gets.chomp

  break if state.empty?

  puts cities[:find].call(cities, state)
end

Basically, I'm just stuck. I can't make the connection between how I type in a state and it returns the city. Any dumbed down explanation would be greatly appreciated.

Upvotes: 2

Views: 302

Answers (3)

Sony Santos
Sony Santos

Reputation: 5545

Here there are a hash (cities) with 3 pairs key => value

cities = {'CA' => 'San Francisco', 'MI' => 'Detroit', 'FL' => 'Jacksonville'}

You can access that values by the key:

puts cities['CA']  #=>  'San Francisco'

Now we add two new pairs.

cities['NY'] = 'New York'
cities['OR'] = 'Portland'

The whole hash will be:

p cities #=> {'CA' => 'San Francisco', 'MI' => 'Detroit', 'FL' => 'Jacksonville', 'NY' => 'New York', 'OR' => 'Portland'}

Now it's defined a method called find_city. It takes a hash and a key.

def find_city (map, state)
  # if hash has the key, return its value. (There are better ways to do it.)
  if map.include? state
    return map[state]
  else
    return "Not found."
  end
end

Here there is the worst piece of Ruby code I've ever seen.

cities[:find] = method(:find_city)

Ok, that code gets the method find_city and turns it into a Method object, which could be assigned to a variable. BUT, instead of using a normal local variable, it is stored in a value of the cities hash!

That object is similar to a method, but it's an object instead of a method, and it must be called with call (in Ruby 1.8.7).

I will use a variable called my_meth (it could be any name) to explain better.

# get the method find_city and turns it into an object assigned to my_meth
my_meth = method(:find_city)

while true
  print "State? (Enter to quit) "
  state = gets.chomp

  break if state.empty?

  # here we use find_city on the cities hash.
  puts my_meth.call(cities, state)
end

puts my_meth.class

But, instead to use a variable, the original code stored the Method object on the cities hash. So, cities will be:

cities[:find] = method(:find_city)
p cities #=> {'CA' => 'San Francisco', 'MI' => 'Detroit', 'FL' => 'Jacksonville', 'NY' => 'New York', 'OR' => 'Portland', :find => (the Method object)}

So, you can access the find_city thru cities[:find].call.

Upvotes: 2

user166390
user166390

Reputation:

A symbol represents itself, just as 0 represents 0 -- and a symbol is only equal to itself. (Consider that :foo == "foo" is false, so hash[:foo] can never refer to the hash["foo"] pair, even if both may evaluate to the same object).

So cities[:find] just doesn't make much sense and it confuses the data (hash mapping sates to a big city) with the operation on the data... it might as well be cityFindFunction instead. However, because using the method itself is fine (no need to convert to a Function here!), consider this simplification:

cities = {'CA' => 'San Francisco', 'MI' => 'Detroit', 'FL' => 'Jacksonville'}

def find_city (map, state)
  if map.include? state
    # if this hash contains the state, e.g. "CA" then
    # return a big city in the state
    return map[state]
  else
    # otherwise return a diagnostic message
    return "Not found."
  end
end

while true
  print "State? (Enter to quit) "
  state = gets.chomp

  break if state.empty?

  # this is a method invocation, no need for "call"!
  puts find_city(cities, state)
end

This could be further simplified with returning nil instead of "Not found." (let the caller deal with it) or using the default-value of a Hash. In any case, I hope that the above shows "the essence" of what is being done.

Happy coding.


A brief bit on a method vs. Method vs Function.

In Ruby, methods are not first class values. Rather, they are "messages" that a given object reacts to. The find_city(cities, state) method invocation above is convenience for self.__send__(:find_city, cities, state). (Well, not really because __send__ is itself a method call, but that's the gist of what happens internally... ;-)

Functions (of type Proc) and objects of type Method, on the other hand, are just objects. As such, unlike methods, they are "first-class values". To invoke/apply a Function/Method object, use the call method (of the Function/Method) which executes the function with the given parameters and evaluates to the Functions/Methods return value.

The method method returns a Method object -- bound to the instance of the object method was invoked upon -- for a given method.

Hope that explains the rest somewhat ;-)

Upvotes: 2

ayckoster
ayckoster

Reputation: 6847

Its not that hard.

  • The first 4 lines fill your hash with abbreviation => name of state
  • Then you define a method find_city that gets a map and an abbreviation and prints the corresponding name.
  • Afterwards you put the method with the name find_city in the hash cities with the key :find.
  • cities[:find].call(cities, state) just calls the previously saved method with the arguments.

Instead of using symbols here you could have used Strings or Integers at least for the hash part. Furthermore you could have used method("find_city") instead of method(:find_city)

You just have to understand basic ruby logic and that you can reference Methods as Objects.

Upvotes: 1

Related Questions