Dec 7, 2015 Tags: programming, ruby
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:
#to_s
is called on a String
(T_STRING
), the object is
returned immediately.#to_s
(idTo_s
) is sent to the object via rb_funcall
.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.
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