Friendly Ruby Objects
Eric Hodel | Thu, 18 Dec 2008 01:28:00 GMT
This post is intended to supplement the Ruby Quickref with the various ways you can make your objects play nicely with each other.
Most of the examples below are taken from RubyGems, some examples won't work until the next release of RubyGems.
Enumerable
The Enumerable module is based on the #each method and contains well known methods like #map and #each_with_index. If the enumerated objects implement the #<=> method you get a useful #sort, #min and #max.
Gem::SourceIndex has a Hash internally that it exposes via #each:
class Gem::SourceIndex
include Enumerable
# ...
def each(&block)
@gems.each(&block)
end
end
Which allows handy things like:
dep = Gem::Dependency.new ARGV.shift, Gem::Requirement.default
found = Gem.source_index.any? do |name, spec|
dep =~ spec
end
puts "found gem for #{dep.name}!" if found
Comparable
The Comparable module is based on the #<=> method and gives all the comparison methods.
Gem::Specification objects are sorted via name, version and platform:
class Gem::Specification
include Comparable
def <=>(other)
my_platform = Gem::Platform::RUBY == @platform ? -1 : 1
other_platform = Gem::Platform::RUBY == other.platform ? -1 : 1
[@name, @version, platform] <=>
[other.name, other.version, other.platform]
end
end
This is used in RubyGems to sort objects both for display on the screen like in gem list and internally when installing gems.
For a way to reduce the repetition in the above code and some other sorting speed-ups, see my post on #sort_by and #sort_obj.
#to_s and #inspect
Overriding #to_s and #inspect prevent you from puking all over the screen when somebody wants to look at your object. To a certain extent limiting the amount of information shown can aid debugging.
Gem::Specification's #to_s that gives only the two most-important attributes:
class Gem::Specification
def to_s
"#<Gem::Specification name=#{@name} version=#{@version}>"
end
end
Gem::Platform's #to_s gives a friendly string:
class Gem::Platform
def to_a
[@cpu, @os, @version]
end
def to_s
to_a.compact.join '-'
end
end
Gem::Version's inspect ignores internal instance variables and only exposes @version:
class Gem::Version
def inspect # :nodoc:
"#<#{self.class} #{@version.inspect}>"
end
end
Case equality with #===, Matching with #=~
While #=== is more commonly overridden, it can also be useful to implement #=~ to allow your objects to be used in a very readable manner.
Gem::Platform overrides #===:
class Gem::Platform
def ===(other)
return nil unless Gem::Platform === other
# cpu
(@cpu == 'universal' or other.cpu == 'universal' or @cpu == other.cpu) and
# os
@os == other.os and
# version
(@version.nil? or other.version.nil? or @version == other.version)
end
end
In short, one platform matches another if they have the same cpu (architecture) or either is universal, they have the same os and their versions match if they have versions.
You can use this to group gems:
platform_count = Hash.new 0
Gem.source_index.each do |name, spec|
case spec.platform
when Gem::Platform.new('linux') then
platform_count['linux'] += 1
# ...
else
platform_count['other'] += 1
end
end
p platform_count
Gem::Dependency overrides #=~. It is a little strange because it converts the right-hand side to a Gem::Dependency object:
class Gem::Dependency
def =~(other)
other = case other
when self.class then
other
else
return false unless other.respond_to? :name and
other.respond_to? :version
Gem::Dependency.new other.name, other.version
end
pattern = @name
pattern = /\A#{Regexp.escape @name}\Z/ unless Regexp === pattern
return false unless pattern =~ other.name
reqs = other.version_requirements.requirements
return false unless reqs.length == 1
return false unless reqs.first.first == '='
version = reqs.first.last
version_requirements.satisfied_by? version
end
end
You can use this as a filter:
dep = Gem::Dependency.new(/ruby/, Gem::Requirement.default)
ruby_named = Gem.source_index.select do |name, spec|
dep =~ spec
end
p ruby_named.map { |name, spec| name }
As a Hash key
Ruby uses #hash and #eql? to determine if two different objects really mean the same hash key.
Gem::Version is usable as a Hash key based on the internal version string:
class Gem::Version
def hash
@version.hash
end
def eql?(other)
self.class === other and @version == other.version
end
end
In Gem::Version, the internal version string looks like "1.3" or "1.3.0". In this implementation the two versions would belong to different hash keys.
Using #eql? instead of #== to determine if two keys are the same is a nice distinction since it allows you to have interesting behaviors (but I'm not sure they are useful). For Gem::Version, a version of "1.3" is equal to "1.3.0", but the occupy different slots in a Hash.
#intialize_copy
#initialize_copy is called during #dup and #clone to copy object-specific state beyond instance variables. The object being cloned from is passed to the new instance. #initialize_copy could be used for cleaning out a per-object cache:
def initialize_copy(other)
@cache = []
end
#exception
#exception is called on objects given to #raise to cast them to Exception objects. It must return a subclass of Exception. You can use this to turn arbitrary objects into exceptions, centralizing all your exception raising code:
class Result
class Error < RuntimeError; end
def initialize(json)
@result = JSON.parse json
end
def exception(message = nil)
Error.new "#{message} (#{@result['error']})"
end
def [](key)
@result[key]
end
end
r = Result.new open('http://example.com/api/blah').read
raise r if r['error']
Marshal
Ruby will marshal most objects automatically, but sometimes you want a custom format to ignore cached data that can be reconstructed or to reduce the size of the data you're saving out. There are two ways to do this, the older way is #_dump/::_load and the newer way is #marshal_dump/#marshal_load which takes priority. If you want to upgrade to the newer way you can leave ::_load to restore older marshaled objects.
Using the older way #_dump returns a String representation of the object (usually another Marshal string) and ::_load receives that String representation. Note that it's a class method, so ::_load is responsible for creating the object, which may be important in some instances.
Using the newer way, #marshal_dump returns an Object and #marshal_load receives that Object. The object is already allocated, but #initialize won't be called. The newer way can result in a smaller marshal dump size since it uses the existing symbol and object reference tables.
Gem::Specification uses #_dump/::load and is fairly complicated because I designed it to be backward and forward-compatible. This is a slightly-stripped-down version:
class Gem::Specification
CURRENT_SPECIFICATION_VERSION = 2
# number of fields per version
MARSHAL_FIELDS = { -1 => 16, 1 => 16, 2 => 16 }
def self._load(str)
array = Marshal.load str
spec = Gem::Specification.new
spec.instance_variable_set :@specification_version, array[1]
# validate object
current_version = CURRENT_SPECIFICATION_VERSION
field_count = if spec.specification_version > current_version then
spec.instance_variable_set :@specification_version,
current_version
MARSHAL_FIELDS[current_version]
else
MARSHAL_FIELDS[spec.specification_version]
end
if array.size < field_count then
raise TypeError, "invalid Gem::Specification format #{array.inspect}"
end
# restore object
spec.instance_variable_set :@rubygems_version, array[0]
# ...
spec.instance_variable_set :@platform, array[16].to_s
spec.instance_variable_set :@loaded, false
spec
end
def _dump(limit)
Marshal.dump [
@rubygems_version,
@specification_version,
# ...
@new_platform,
]
end
end
Gem::Version uses #marshal_dump/#marshal_load and ignores the internal instance variables, only dumping @version:
class Gem::Version
def marshal_dump
[@version]
end
def marshal_load(array)
self.version = array[0]
end
end
Pretty-print with PP
With a little work PP can give you easily readable output for your objects, even output that you can copy and paste back into a script. Primarily you'll use the PrettyPrint#group, PrettyPrint#text, PrettyPrint#breakable and PP#pp methods inside a #pretty_print method on your object. You can find documentation for these methods using ri.
Here's #pretty_print from Gem::Dependency and Gem::Requirement:
class Gem::Dependency
def pretty_print(q)
q.group 1, 'Gem::Dependency.new(', ')' do
q.pp @name
q.text ','
q.breakable
q.pp @version_requirements
q.text ','
q.breakable
q.pp @type
end
end
end
class Gem::Requirement
def pretty_print(q)
q.group 1, 'Gem::Requirement.new(', ')' do
q.pp as_list
end
end
def as_list
normalize
@requirements.map do |op, version| "#{op} #{version}" end
end
end
Together these make pretty, copy-pastable output:
require 'pp'
gem 'ParseTree'
pp Gem.loaded_specs["ParseTree"].dependencies
[Gem::Dependency.new("RubyInline",
Gem::Requirement.new([">= 3.7.0"]),
:runtime),
Gem::Dependency.new("sexp_processor",
Gem::Requirement.new([">= 3.0.0"]),
:runtime),
Gem::Dependency.new("hoe", Gem::Requirement.new([">= 1.8.0"]), :development)]
RubyGems 1.3.1
Eric Hodel | Thu, 13 Nov 2008 21:44:00 GMT
NOTE: RubyGems 1.1 and 1.2 have problems upgrading when there is no rubygems-update installed. You will need to follow the second set of update instructions if you see “Nothing to update”.
Release 1.3.1 fixes some bugs.
Bugs fixed:
- Disregard ownership of ~ under Windows while creating ~/.gem. Fixes issues related to no uid support under Windows.
- Fix requires for Gem::inflate, Gem::deflate, etc.
- Make Gem.dir respect :gemhome value from config. (Note: this feature may be removed since it is hard to implement on 1.9.)
- Kernel methods are now private. Patch #20801 by James M. Lawrence.
- Gem::location_of_caller now behaves on Windows. Patch by Daniel Berger.
- Silence PATH warning.
Deprecation Notices:
- Gem::manage_gems will be removed on or after March 2009.
For a full list of changes to RubyGems and the contributor for each change, see the ChangeLog file.
Special thanks to Chad Wooley for backwards compatibility testing and Luis Lavena for continuing windows support.
How can I get RubyGems?
NOTE: If you have installed RubyGems using a package system you may want to install a new RubyGems through the same packaging system.
If you have a recent version of RubyGems (0.8.5 or later), then all you need to do is:
$ gem update --system (you might need to be admin/root)
NOTE: RubyGems 1.1 and 1.2 have problems upgrading when there is no rubygems-update installed. You will need to follow the second set of update instructions if you see “Nothing to update”.
NOTE: You may have to run the command twice if you have any previosly installed rubygems-update gems.
If you have an older version of RubyGems installed, then you can still do it in two steps:
$ gem install rubygems-update (again, might need to be admin/root) $ update_rubygems (... here too)
If you don’t have any gems install, there is still the pre-gem approach to getting software … doing it manually:
- DOWNLOAD FROM: http://rubyforge.org/frs/?group_id=126
- UNPACK INTO A DIRECTORY AND CD THERE
- INSTALL WITH: ruby setup.rb (you may need admin/root privilege)
To File Bugs
The RubyGems bug tracker can be found on RubyForge at: http://rubyforge.org/tracker/?func=add&group_id=126&atid=575
When filing a bug, `gem env` output will be helpful in diagnosing the issue.
If you find a bug where RubyGems crashes, please provide debug output. You can do that with `gem—debug the_command`.
Keep those gems coming!
—Jim & Chad & Eric (for the RubyGems team)
Animal Verbing
Eric Hodel | Tue, 21 Oct 2008 13:58:00 GMT
Yesterday I was sitting with a bunch of Smalltalkers listening to them talk about Smalltalk Superpowers. They described rather mundane things like sender sender sender receiver which the receiver of the method that called into this library higher up the stack to fancier things like super super which allows you to skip over the parent classes when calling super to really fancy things like getting the address of an Object.
When Ryan told the Smalltalkers about our superpowers of duck typing, duck raping and monkey patching, the main response was “what’s with all the animal verbing?” These kinds of tricks seem so commonplace to the Smalltalkers that they have no names for them.
Why is it that Rubyists went with “animal verbing” for describing these practices?
PS: One of the things Ruby has that’s rather difficult in Smalltalk is object customization, o = Object.new; def o.my_method() end
Update: You can download videos of the various superpowers, or visit the official Smalltalk Superpowers website.
UPnP-MediaServer 1.0.0
Eric Hodel | Wed, 23 Jul 2008 08:41:00 GMT
UPnP-MediaServer version 1.0.0 has been released!
A UPnP MediaServer. Currently a work in progress. Only tested on a PlayStation 3.
Changes:
1.0.0 / 2008-07-23
- 1 major enhancement
- Birthday!
UPnP-ContentDirectory 1.0
Eric Hodel | Wed, 23 Jul 2008 08:40:00 GMT
UPnP-ContentDirectory version 1.0 has been released!
A UPnP ContentDirectory service with some DLNA extensions. Currently this is a work in progress, and is only adequate for viewing images on a PlayStation 3.
Changes:
1.0.0 / 2008-07-23
- 1 major enhancement
- Birthday!
UPnP-ConnectionManager 1.0
Eric Hodel | Wed, 23 Jul 2008 08:38:00 GMT
UPnP-ConnectionManager version 1.0 has been released!
Stub implementation for a UPnP ConnectionManager service. Currently has no implementation. Works great for a PlayStation 3.
Changes:
1.0.0 / 2008-07-23
- 1 major enhancement
- Birthday!
UPnP 1.1.0
Eric Hodel | Wed, 23 Jul 2008 08:36:00 GMT
UPnP version 1.1.0 has been released!
An implementation of the UPnP protocol
Changes:
1.1.0 / 2008-07-23
- 2 major enhancements
- Server support
- SSDP now supports sending advertisements
- 1 bug fix
- Gem dependencies now listed
UPnP-IGD 1.0.0
Eric Hodel | Mon, 30 Jun 2008 06:45:00 GMT
UPnP-IGD version 1.0.0 has been released!
UPnP-IGD is a UPnP extension for Internet Gateway Devices that displays information about the device. UPnP-IGD is an example of the use of some UPnP APIs.
UPnP-IGD comes with the upnp_igd utility which outputs information like this:
Gateway at http://10.1.1.1/ Connected, up since 2008-06-29 03:31:00 (66123 seconds) Connection type: IP_Routed 384 Kb/s up, 1536 Kb/s down External IP address: 10.255.255.254 Port mappings: 0 UDP *:4502 -> 10.1.1.2:4500 "iC4502" 1 UDP *:5355 -> 10.1.1.2:5353 "iC5355" 2 UDP *:58712 -> 10.1.1.3:58712 "Skype UDP at 10.1.1.3:58712 (546)" 3 TCP *:58712 -> 10.1.1.3:58712 "Skype TCP at 10.1.1.3:58712 (546)" 4 UDP *:5353 -> 10.1.1.4:5353 "iC5353"
Unfortunately, UPnP-IGD does not allow anything to be set on the device, and has only been tested against miniupnpd.
UPnP 1.0.0
Eric Hodel | Mon, 30 Jun 2008 06:09:00 GMT
UPnP version 1.0.0 has been released!
UPnP-1.0.0 is an implementation of the UPnP protocol. The 1.0 release:
- Discovers UPnP devices and services via SSDP, see UPnP::SSDP
- Creates a SOAP RPC driver for discovered services, see UPnP::Control::Service
- Creates concrete UPnP device and service classes that may be extended with utility methods, see UPnP::Control::Device::create, UPnP::Control::Service::create and the UPnP-IGD gem.
- Does not support eventing
- Does not support server creation
UPnP comes with upnp_discover which searches for UPnP devices and dumps information about them and upnp_listen which will listen for device notifications.
Gem Dependencies
Eric Hodel | Mon, 23 Jun 2008 22:28:40 GMT
I was chatting with Yehuda Katz, and somehow we ended up talking about speed:
Eric Hodel: I don’t worry about inheritance
Eric Hodel: or speed, for that matter
Yehuda Katz: heh
Yehuda Katz: that’s why you wrote RubyInline?
Eric Hodel: Ryan wrote RubyInline because, roughly, “How hard could it be?”
Eric Hodel: and an intense hatred of C
This got me thinking, how long did it take from RubyInline to be written until it was actually used? The RubyGems answer is 151 days, and by ParseTree, but RubyInline is actually about 2 years older than its gem, first committed on 2002/09/05.
To figure this out, I wrote a script that walks all the gems and figures out when a gem was first released, when a gem was first mentioned in a dependency, and the time between the two. Today, the figures are:
681 of 3234 gems used as dependencies (21%) average time to first use is 148 days maximum time to first use is 1332 (dnssd)
You can get the full output in the gem dependency report (which updates weekly), and download the gem dependency script too.

Articles