Creating a Shared Whiteboard
Credit: James Edward Gray II
Problem
You want to create the network equivalent of a whiteboard. Remote programs can place Ruby objects up on the board, examine objects on the board, or remove objects from the board.
Solution
You could just use a synchronized hash (as in Recipe 16.10), but Rinda[6] provides a data structure called a TupleSpace that is optimized for distributed programming. It works well when you have some clients putting data on the whiteboard, and other clients processing the data and taking it down.
[6] Rinda is a companion library to DRb. Its a Ruby port of the Linda distributed computing environment, which is based on the idea of the tuplespace. Its similar to JavaSpaces.
Lets create an application that lets clients on different parts of the network translate each others sentences, and builds a translation dictionary as they work.
Its easier to see the architecture of the server if you see the clients first, so heres a client that adds some English sentences to a shared TupleSpace:
#!/usr/bin/ruby -w # english_client.rb require drb require inda/tuplespace # Connect to the TupleSpace… DRb.start_service tuplespace = Rinda::TupleSpaceProxy.new( DRbObject.new_with_uri(druby://127.0.0.1:61676) )
The English clients job is to split English sentences into words and to add each sentence to the whiteboard as a tuple: [unique id, language, words].
counter = 0 DATA.each_line do |line| tuplespace.write([(counter += 1), English, line.strip.split]) end __END__ Ruby programmers have more fun Ruby gurus are obsessed with ducks Ruby programmers are happy programmers
Heres a second client. It creates a loop that continually reads all the English sentences from the TupleSpace and puts up word-for-word translations into Pig Latin. It uses Tuplespace#read to read English-language tuples off the whiteboard without removing them.
require drb require inda/tuplespace require set DRb.start_service tuplespace = Rinda::TupleSpaceProxy.new( DRbObject.new_with_uri(druby://127.0.0.1:61676) ) # Track of the IDs of the sentences weve translated translated = Set.new # Continually read English sentences off of the board. while english = tuplespace.read([Numeric, English, Array]) # Skip anything weve already translated. next if translated.member? english.first translated << english.first # Translate English to Pig Latin. pig_latin = english.last.map do |word| if word =~ /^[aeiou]/i "#{word}way" elsif word =~ /^([^aeiouy]+)(.+)$/i "#{$2}#{$1.downcase}ay" end end # Write the Pig Latin translation back onto the board tuplespace.write([english.first, Pig Latin, pig_latin]) end
Finally, heres the language server: the code that exposes a TupleSpace for the two clients to use. It also acts as a third client of the TupleSpace: it continually takes non-English sentences down off of the whiteboard (using the destructive TupleSpace#take method) and matches them word-for-word with the corresponding English sentences (which it also removes from the whiteboard). In this way it gradually builds an English-to-Pig Latin dictionary, which it serializes to disk with YAML:
#!/usr/bin/ruby -w # dictionary_building_server.rb require drb require yaml require inda/tuplespace # Create a TupleSpace and serve it to the world. tuplespace = Rinda::TupleSpace.new DRb.start_service(druby://127.0.0.1:61676, tuplespace) # Create a dictionary to hold the terms we have seen. dictionary = Hash.new # Remove non-English sentences from the board. while translation = tuplespace.take([Numeric, /^(?!English)/, Array]) # Match each with its English equivalent. english = tuplespace.take([translation.first, English, Array]) # Match up the words, and save the dictionary. english.last.zip(translation.last) { |en, tr| dictionary[en] = tr } File.open(dictionary.yaml, w) { |file| YAML.dump(dictionary, file) } end
If you run the server and then the two clients, the server will spit out a dictionary.yaml file that shows how much it has already learned:
$ ruby dictionary_building_server.rb & $ ruby english_client.rb $ ruby pig_latin_client.rb & $ cat dictionary.yaml --- happy: appyhay programmers: ogrammerspray Ruby: ubyray gurus: urusgay ducks: ucksday obsessed: obsessedway have: avehay are: areway fun: unfay with: ithway more: oremay
Discussion
Rindas TupleSpace class is pretty close to the network equivalent of a whiteboard. A "tuple" is just an ordered sequencein this case, an array of Ruby objects. A TupleSpace holds these sequences and provides an interface to them.
You can add sequences of objects to the TupleSpace using TupleSpace#write. Later, the same or different code can query the object using TupleSpace#read or TupleSpace#take. The only difference is that TupleSpace#take is destructive; it removes the object from the TupleSpace as its read.
You can select certain tuples by passing TupleSpace#read or TupleSpace#take a template that matches the tuples you seek. A template is just another tuple. In the example code, we used templates like [Numeric, English, Array]. Each element of a tuple is matched against the corresponding element of a template with the === operator, the same operator used in Ruby case statements.
That particular template will match any three-element tuple whose first element is a Numeric object, whose second element is the literal string English, and whose third element is an Array object: that is, all the English sentences currently on the whiteboard.
You can create templates containing any kind of object that will work with the === operator: for instance, a Regexp object in a template can match against strings in a tuple. Any nil slot in a template is a wildcard slot that will match anything.
See Also
- The DRb presentation by Mark Volkmann in the "Why Ruby?" repository at http://rubyforge.org/docman/view.php/251/216/DistributedRuby.pdf has some material on TupleSpaces
- Clients can also choose to be notified of TupleSpace events; you can see an example at http://ruby-talk.org/cgi-bin/scat.rb/ruby/ruby-talk/159065
Категории