@CapnKernul writes:
[do you know of] a Ruby idiom for converting an object to a type if it isn’t already that type. For example, if you want to only store an attribute of type Foo, you could write an accessor method that would pass any non-Foo object to Foo’s constructor.
There is a de facto idiom among Ruby built-ins to define a method with the same name as a class for doing conversions to that class. My favorite is Kernel#Array
:
Array("foo") # => ["foo"] Array([1,2,3]) # => [1, 2, 3] Array(nil) # => [] Array({:a => 1, :b => 2}) # => [[:a, 1], [:b, 2]]
But there are others. Kernel#Integer
provides a stricter form of integer conversion than does #to_i
:
"42".to_i # => 42 Integer("42") # => 42 "30 seconds".to_i # => 30 Integer("30 seconds") # => # ~> -:4:in `Integer': invalid value for Integer(): "30 seconds" (ArgumentError) # ~> from -:4:in `<main>'
And there are also Kernel#String
, Kernel#Float
, Kernel#Complex
, and Kernel#Rational
:
String(123) # => "123" Float("123") # => 123.0 Rational(4,5) # => (4/5) Complex(1,3) # => (1+3i)
You can also find this in standard libraries. The URI library defines an idempotent conversion method for URIs:
require 'uri' u = URI("http://example.com") # => #<URI::HTTP:0x000000030cac80 URL:http://example.com> URI(u) # => #<URI::HTTP:0x000000030cac80 URL:http://example.com>
There’s also Pathname
:
require 'pathname' p = Pathname("~/.emacs.d") # => #<Pathname:~/.emacs.d> Pathname(p) # => #<Pathname:~/.emacs.d>
I like to extend this idiom into my own code for idempotent conversions into commonly-used value objects. I did this in Objects on Rails for TagList objects.
module Conversions private def TagList(value) return value if value.is_a?(TagList) TagList.new(value) end end
The TagList
method is shorter than calling TagList.new(...)
. And in addition, it’s safe to call on inputs which might or might not already be TagList
objects. Although they may look a little odd at first, capitalized conversion methods are a well-established Ruby idiom for methods which “do the right thing” to convery any reasonable input value into a desired class. And because unlike #to_x
methods they are outside of the objects being converted, they incur none of the maintenance danger of a monkey-patch. I don’t define them for every class in my program; but for oft-used value object types they can be quite convenient.
I’ve always been intrigued by the syntax for the hash conversion syntax, e.g. Hash[“a”, 100, “b”, 200], which seems to serve the same role, but uses square brackets for the conversion.
Dir
also has aDir.[]
, basically aDir.glob
shortcut. Not sure if there’s a convention there.Unlike Hash, I think Dir#[] actually makes sense. The implication of square brackets is to “find” or “select” some object or subset. In this case, Dir[“*.rb”] means “select the subset of filenames corresponding to this glob pattern”.
I like this too, especially since you can define it on the class itself. Seems more contained this way.
I rather dislike the Hash syntax. It feels like an abuse of square brackets to me. I realize Ruby plays fast and loose with square brackets, but I feel like in general they imply a “finder” or “selector” method: “find the object matching this pattern”.
Yeah, it surprised me when I first saw it, for that same reason of not matching the other uses of square brackets elsewhere in Ruby.
I’m aware this is an old post but still felt like pitching in here. It’s not an uncommon pattern in Ruby to have square brackets on a collection class act as a constructor. Examples that do this are Set, Matrix, Hash, Array. Some custom collections like Hamster::List also have this.
It’s a different use from “find”, I read it more as “grab these things together in collection of type X”.
I’m not a ruby programmer but if I understand the scenarios above I mostly try follow the 2 conversion idioms that Kent Beck wrote in his Smalltalk Best Practice Patterns book.
For objects that share the same protocol but different format create a conversion method on the object and prepend ‘as’ to the class of the returned object. e.g. Collection>>asSet
For conversion of an object to another object with a different protocol make a creation converter method that takes the object to be converted e.g. Date class>>fromString:
This being said I’d stick with the current Ruby idioms if they are well understood as changing it would only bring confusion, but on a team if you agree on the convention then I like the 2 idioms that Kent introduced.