ruby picture

RCR 317: klass.from_s(str) or more generally klass.from_*(...)

Submitted by eric_mahurin (Sun Sep 11 19:49:18 UTC 2005)

Abstract

Provide a more general methodology/convention for converting to/from Strings and other core classes. The same scheme could be used for converting to/from other non-core classes.

Problem

Currently, the convention for handling conversions from a string to an arbitrary class is to designate an abbreviation for that class and add a new String#to_ method. This results in poor encapsulation (because some of the class code is in String), clutter in the String class, and the need to map a class to an abbreviation or String convertor method when dealing with an arbitrary class.

Proposal

In addition to the current klass#to_* methods, provide klass.from_*(obj) methods where appropriate. I'm also thinking that these from_* methods should raise an exception when the entire from object doesn't convert (unlike String#to_i for example).

Analysis

The main benefit of this proposal would be the ability to convert from a specific known class (usually String) to an arbitrary class that can support this conversion. Assuming we are talking about converting from a String to an arbitrary class, you could do this:

obj = klass.from_s(str)

Currently, we can easily convert from an arbitrary object to a specific class as long as the appropriate #to_* method is supported. With this RCR, we can easily go the other way: from an object of a specific type to an arbitrary class - as long as that class support the appropriate klass.from_* method.

You could also consider using this framework to provide general klass.from and klass#to methods to convert from an arbitrary object to an object of an arbitrary class. But, I would question the value of the much generality and the implementation might be a bit more kludgy.

You can also compare this RCR to:

https://rcrchive.net/rcr/show/280

The above RCR is quite a bit more complex than this one, but it allows a little more generality - from arbitrary class/type to arbitrary class/type. I see this currently proposed RCR as a very simple compromise between what we have now and RCR 280. And as seen in the implementation, it is much simpler.

Here is a great example of about the simplest command-line option parser you can get. Every option has a default value and if the option appears on the command-line, the option value (string) is convert to an object of the same class of the default.

 def argv_options(options)
    i = 0
    while arg = ARGV[i]
        if arg[0]==?-
            arg.slice!(0)
            ARGV.slice!(i)
            break if arg=="-" # -- terminates options
            opt = arg.to_sym
            default = options[opt]
            if default
                klass = default.class
                options[opt] = klass.from_s(ARGV.slice!(i))
            elsif default.nil?
                raise("unknown option -#{opt}")
            else # default==false
                options[opt] = true
            end
        else
            i += 1
        end
    end
    options
 end
 ARGV.replace(%w(
    -n 4
    -multiplier 3.14
    -q
    -title foobar
    -pattern fo+
    -time 5:55PM
    -method downcase
    -filter |x|x*x
    a b c
 ))
 options = argv_options(
    :n          => 1,
    :multiplier => 1.0,
    :q          => false,
    :title      => "hello world!",
    :pattern    => /.*/,
    :time       => Time.new,
    :method     => :to_s,
    :default    => 123,
    :filter     => Proc.new {|x|x}
 )

Without these klass.from_s methods, you would need a big case statement in the example above for the klass.from_s and the caller couldn't extend to arbitrary types. Or you would have to specify the conversion method in the option spec.

Implementation

 def Float.from_s(s)
     Float(s)
 end
 def Integer.from_s(s)
     Integer(s)
 end
 def Symbol.from_s(s)
     s.to_sym
 end
 def String.from_s(s)
     s.to_s
 end
 def Regexp.from_s(s,*other)
     new(s,*other)
 end
 def Proc.from_s(s,*binding)
     eval("new {#{s}}",*binding)
 end
 require 'time.rb'
 def Time.from_s(s,*other)
     Time.parse(s,*other)
 end

This is really just a convention to be followed for classes that want conversion to/from some specific class(es). The focus of above is to/from strings (the most useful).

ruby picture
Comments Current voting
As I said on ruby-talk, I think this is a bad idea. I don't think that this is something that can cleanly be generalised, and I really don't think that to_* methods can be properly expressed in terms of from_* in any case. The needs and purposes of each may be subtly different. The more I use Ruby, the less that I think that a general-purpose conversion environment is valuable. Additionally, I believe that others have expressed more clearly *just* why this is a bad idea in the original thread regarding this concept. -Austin


As I said in this RCR, I'm only attempting to generalize converting from a known specific type to an arbitrary type. I don't think you can get much cleaner than the implementation I gave - just add some class from_* methods. Unlike other solutions, this one is very simple and coexists with the current instance to_* methods (which converts from an arbitrary type to a known specific type). I'm not proposing that anything change (or be deprecated) about the current to_* methods.


I'm not sure how often I want to convert strings to instances of classes. At least, I don't remember the occasion in my 10 years of Ruby programming.

- matz.


Matz, I assume you mean to instances of arbitrary classes, because I'm sure you've converted strings to Integers, Float, and Symbols. An example I gave on ruby-talk was a general command-line parser. The command-line starts as a bunch of strings. In your command-line spec, you may describe what arguments/options are what type (the spec would have the klass). Then you'd just call klass.from_s(s) to convert. Of course another solution to that problem would be to specify the conversion method (:to_i, :to_f, :to_s, etc), but it doesn't seem as clean. I'm sure others who have brought up having a general conversion mechanism have other examples. Since I've seen the conversion topic multiple times and the above seems like a very simple solution (that solved most cases), I thought I'd propose it.

I'm very surprised that the more complex RCR 280 has more support than this RCR.

Eric


I just don't get how str.to_i is cleaner than Integer.from_s(str), where we can use symbols to call methods. Besides that, I don't think generic from_s/to_s combination works for arbitrary classes. We have marshal (or something like YAML) for the purpose.

- matz.


Correct, klass.from_s doesn't work for arbitrary classes, because not all classes have a good way to convert a simple string to an instance of their class (in fact, most don't). This only applies to classes that have a good from_s method (or from_*).

BTW, I think this RCR is very similar to the Kernel methods #Integer, #Float, and #String. So if you have an arbitrary class (klass) that supports conversion from strings, you could do this:

  send(klass.to_s.to_sym,str)

Assuming a method named the same as klass is in Kernel. This RCR has these methods more appropriately as class methods of klass instead of in Kernel.


 --- Jim Freeze wrote:
 > That's pretty interesting Eric, to grab the type off the
 > default.
 > I think I'll add that to CommandLine::OptionParser.
 > 
 > However, I'm still not sure if I like the #from_s form, 
 > but I can see the utility of it. For the common cases, 
 > I can use a simple case statement:
 > 
 >   case default
 >     when Float then Float(arg)
 >     when Fixnum then Integer(arg)
 >   end
 > 
 > But, as you can see with even these simple cases, there
 > are big issues and big questions to answer.
 > 1. Fixnum does not match Integer 

No problem. Fixnum inherits from Integer. Do you care whether Fixnum.from_s returns a Fixnum or a Bignum? It could return either just like many of the other Fixnum instance methods.

 > 2. Do we use to_i or Integer(#) - Integer raises and to_i does not
 > 3. Do we use Float or to_f - Float raises and to_f does not

Good point. This RCR should to specify this. I would think it best if an exception occur if the full string doesn't parse the the target type. I'll change the implementation to use the methods that raise exceptions.

 > Then, there are the tougher cases like
 > 
 >   require 'parsedate'
 >   case default
 >     when Time then Time.gm(*ParseDate.parsedate(arg))

I haven't dealt with dates and times to know what all the options are. I threw this in at the last minute. If you don't like the klass.from_s method, you could define your own derived class (or override that klass.from_s):

 require 'parsedate'
 class MyTime < Time
   def self.from_s(s)
     gm(*ParseDate.parsedate(s))
   end
 end

and then make the default be a MyTime instead of a Time.

 >     when Fixnum arg.to_i  # what if yield bignum

answered above

 >   end
 > 
 > where we have to use a help class and helper method (not new)
 > to get
 > the object we want. Or if the conversion method
 > returns something other than what we requested, like
 > Bignum instead of Fixnum.
 > 
 > Sadly, the #from_s RCR doesn't seem to address any of these
 > issues.

Eric


I don't think Integer.from_s(x) etc. are "methods more appropriately as class methods of klass instead of in Kernel". They are just not same. Besides that, sometimes functional form is more appealing than class method form.

- matz.


If you haven't noticed, I added an example that shows this RCR in action. I think this is the cleanest/most flexible solution with the given super-simple command line option spec: option => default (option value converted to default.class when given).


I see your example being neat. But I'm not sure it's enough to add from_s methods to the core to save a few lines of case statement.

In addition, I don't like the name "from_s".

- matz.


Fair enough. Maybe it'll go in some miscellaneous rubyforge project if it doesn't make it into std or built-in libs. I'm not at all tied to the name from_*. It just seemed to be a good complement to #to_*.


The concept of #from_s seems sound to me, from the standpoint of where methods belong. If you import an arbitrary class that can translate itself to/from a string, the class itself should have methods on it for that purpose, NOT muddy up the built-in string class to add them.

my_obj = MyClass.from_s( the_str ); my_obj.to_s

This should only exist in the case where a 1-1 correspondence exists between an instance of a class an its string representation. "foo bar" and "foo, bar" could both mean %w|foo bar|, so Array.from_s doesn't make sense, IMO.

Further, it makes sense to me that the set of core classes MAY cross the boundaries for simple interoperability. String#to_i makes sense, because String and Integer are both in the core.

In short - I think #from_s is a good pattern for your own custom classes, but doesn't need to be imposed on the core. It might be nice for consistency, but it wouldn't be consistent due to items like Array. - GavinKistner


I agree that Array.from_s doesn't make a lot of sense and I'm not proposing it.

Regarding naming, another option would be to put the word "new" in the name because it makes a new object of the class. Like perl's new_from_fd, it could be new_from_s. But, I still like simply from_s better.

A crazy idea (not putting it in the proposal) would be to make a default for this:

 class Class
   def from_s(s)
     eval("new(#{s})")
   end
 end

but you'd want to somehow make sure s didn't have any variable references in it (including indirectly to self).


Strongly opposed 4
Opposed 1
Neutral 0
In favor 0
Strongly advocate 1
ruby picture
If you have registered at RCRchive, you may now sign in below. If you have not registered, you may sign up for a username and password. Registering enables you to submit new RCRs, and vote and leave comments on existing RCRs.
Your username:
Your password:

ruby picture

Powered by .