ENOSUCHBLOG

Programming, philosophy, pedaling.


Mini-post: Abusing to_s for Brevity in Ruby

Dec 7, 2015     Tags: programming, ruby    

This post is at least a year old.

In Ruby, a common design pattern for modules and classes that don’t require unique object state is the use of “static” (in the sense of Java) methods. I’ve used this style in a few of my own projects, including in my port of the Upworthy Generator and in a music genre generator:

1
2
puts Upworthy.headline # => "What this disgraced former model did is genius"
puts GenreGen.generate # => "Improvised french live disco"

This pattern makes a lot of intuitive sense - we don’t waste time or resources allocating a new Upworthy or GenreGen every time we want a single String, and the meaning in each is fairly obvious.

However, using this pattern repeatedly has made me aware of its (relative) verbosity. In my use case, the programmer or user only interacts with the module through a single method, retrieving only a string.

Why not, then, simply define self.to_s and let Ruby’s lovely duck typing sort things out for us?

A cursory experiment suggests that it works:

1
2
3
4
5
6
7
module Foo
	def self.to_s
		"hello"
	end
end

puts Foo # => "hello"

Interestingly, however, this doesn’t (ostensibly because to_s is expected to return a String type):

1
2
3
4
5
6
7
module Foo
	def self.to_s
		Time.now
	end
end

puts Foo # => #<Module:0x00000000000000>

This is confirmed by rb_obj_as_string in string.c:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
VALUE
rb_obj_as_string(VALUE obj)
{
	VALUE str;

	if (RB_TYPE_P(obj, T_STRING)) {
	return obj;
	}
	str = rb_funcall(obj, idTo_s, 0);
	if (!RB_TYPE_P(str, T_STRING))
	return rb_any_to_s(obj);
	if (!FL_TEST_RAW(str, RSTRING_FSTR) && FL_ABLE(obj))
	/* fstring must not be tainted, at least */
	OBJ_INFECT_RAW(str, obj);
	return str;
}

An explanation:

  1. If #to_s is called on a String (T_STRING), the object is returned immediately.
  2. Otherwise, #to_s (idTo_s) is sent to the object via rb_funcall.
  3. If the returned value is not a String, rb_any_to_s (the generic stringifier) is called on obj and its result is returned instead.

This behavior is a little strange (attempting to stringify str in the code above with ruby_any_to_s makes more sense to me), but it doesn’t interfere with the proposed pattern so long as we’re careful to only return Strings.

Conclusions

Based on the observations above, the first code example could easily be rewritten as

1
2
3
# optional: .to_s on the end
puts Upworthy
puts GenreGen

…provided that self.to_s is defined (or aliased) to the proper string producing method.

Is this more readable? I don’t know. It’s definitely shorter, but it also doesn’t feel quite like Ruby to me.

I don’t think I’ll switch to this style for any of my modules, but it’s certainly worth keeping in mind as an example of clever duck typing (and of #to_s edge-case behavior).

- William