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)]

Posted in ,  | 6 comments

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:

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)

Posted in , ,  | no comments

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.

Posted in  | 3 comments

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!

Posted in ,  | no comments

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!

Posted in ,  | no comments

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!

Posted in ,  | no comments

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

Posted in ,  | no comments

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.

Posted in ,  | no comments

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.

Posted in ,  | 3 comments

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.

Posted in ,  | no comments

Older posts: 1 2 3 4 5 ... 11