Crazy thought -- Rubyish type conversion?

Daniel Schierbeck wrote:

Hmmm, I'm not too fond of the use of classes -- I'd like to use the #to_* methods instead. Otherwise the dynamic typing will suffer.

Nope, not at all. ruby-contract uses the same adaption routes that Ruby uses:

  # Built-in adaption routes that Ruby already uses in its C code.
  adaption :to => Symbol, :via => :to_sym
  adaption :to => String, :via => :to_str
  adaption :to => Array, :via => :to_ary
  adaption :to => Integer, :via => :to_int

So if it expects a Symbol anywhere it will accept a Symbol or any object that has to_sym() and so on. In theory you could also add non-standard adaption routes (like String => StringIO), but when you add more and more of them you also add more and more sources of hard to detect bugs to your application.

Perhaps this?

  def foo(<str> bar, <int> baz, <sym> bur); end

Although I still think this is more terse:

  def foo(str bar, int baz, sym bur); end

The downside of that is that you can't anymore use any expression. With eh <> syntax you can also do this:

def foo(<type_for(:foo, 1)> bar)
def foo(<foo bar> qux)

At least the last one can't be done without an explicit delimiter. It might be rare and exotic, but IMHO one of the great things of Ruby's syntax is that you can use arbitrary expressions all over the place.

···

--
http://flgr.0x42.net/

transfire@gmail.com wrote:

That's pretty close really. If you can do this (w/o Binding.of_caller
though b/c it's too fragile).

   def foo(bar, baz, bur)
     signature String, Integer, Symbol
     # something
   end

Disagreed. I think the signature information should be available before you call the method.

Please note that ruby-contract also adds a signature() method to the Method and UnboundMethod classes. That would not be possible when the signature is inside the actual method. I also think that you can't get the above syntax to work because argument order information will be lost.

And I don't think explicit conversion belongs into a type library. It's something that ought to be explicitly done by the user IMHO.

···

--
http://flgr.0x42.net/

Hi --

Hi --

It sounds like you're talking about class, rather than type, or
(since, as you say, to_x doesn't actually have to return an X) just a
kind of convenience method-call layer. I'd rather see the methods
spelled out.

No, I don't mean class -- I think of "type" as the context of an object, one that cannot always be deducted by it's methods:

phone.call
method.call

these two #call methods are in different contexts -- I think of the #to_* methods as a way to determine that context.

But that's a different matter from either the class or the type of
phone and/or method.

Is behavior not relevant to type?

I think I'm not understanding what you mean by the context of an
object. Your example involves two different objects (I assume), so
I'm not clear on which object's context/type you're referring to. Or
do you mean the context in which a given method name appears?

Don't forget, though, the "to" in to_* really means a *different*
object. It's not the same object behaving differently.

str = "foo"
str.object_id #=> 23456248224800
str.to_str.object_id #=> 23456248224800

Well, at least it's not behaving differently :slight_smile:

The built-in to_* methods are very class-specific, and in a sense are
not the best advertising for duck typing, though they are very
convenient and they're closely tied in with the most basic built-in
classes.

I don't think they have to be tied in with the classes, and I think we should try to avoid getting type mixed up with class. When I call #to_x, I expect to get an X type, meaning that I can call the methods defined by X on the object, and the object responds in an expected manner. I don't care if the object actually *is* an instance of X, just as long as is keeps its side of the deal and responds to the methods I throw at it.

I can see why some of the low-level methods might actually need an actual String or Numeric instance, but for high-level code I just don't see the point -- it's not like knowing the class means you can be any more sure that no-one has tampered with it.

What I mean is: there's already a convention of using to_* for these
class-specific conversion methods, so if you want to create a system
of true type (as opposed to class) conversions, to_* might be a
misleading name.

In other words, I'm not suggesting that how the core to_* methods work
should be a model for how everything should work, but only that
anything that doesn't work the way they work maybe shouldn't be called
to_*.

David

···

On Mon, 29 May 2006, Daniel Schierbeck wrote:

dblack@wobblini.net wrote:

On Sun, 28 May 2006, Daniel Schierbeck wrote:

dblack@wobblini.net wrote:

--
David A. Black (dblack@wobblini.net)
* Ruby Power and Light, LLC (http://www.rubypowerandlight.com)
   > Ruby and Rails consultancy and training
* Author of "Ruby for Rails" from Manning Publications!
   > Ruby for Rails

Ross Bamford wrote:

···

On Sun, 2006-05-28 at 19:37 +0900, Daniel Schierbeck wrote:

That's very interesting. Would it be better if class objects were given instead of class names? Otherwise it could be tedious to convert to A::b::BadgerMilk :slight_smile:

Good point :slight_smile: The way it's written, you can do that already, since the
'syms' are all to_s'd anyway.

$intclz = Integer # just for example

def ameth(foo, bar, baz, &blk)
  argsto($intclz, String, :sym)
  blk.call(foo, bar, baz)
end

I don't think that'll work with "nested" constants, i.e. A::b::C. I think it may complicate things too much to allow classes/class names -- besides, it should be the arguments, not the classes, that should handle the conversion.

Florian Groß wrote:

... ruby-contract uses the same adaption routes that Ruby uses:

  # Built-in adaption routes that Ruby already uses in its C code.
  adaption :to => Symbol, :via => :to_sym
  adaption :to => String, :via => :to_str
  adaption :to => Array, :via => :to_ary
  adaption :to => Integer, :via => :to_int

So if it expects a Symbol anywhere it will accept a Symbol or any object that has to_sym() and so on. In theory you could also add non-standard adaption routes (like String => StringIO), but when you add more and more of them you also add more and more sources of hard to detect bugs to your application.

That's exactly what I think is the problem; you have to tell Ruby how to convert an object to another type anyway, so why not just use the established convention? Besides, I don't think it should be mandatory for all #to_* methods to return an * object -- I think it would be completely okay if #to_a returned an object that responded to all messages that an Array object would, in the same manner (e.g. #each yielded a single object.)

  def foo(str bar, int baz, sym bur); end

The downside of that is that you can't anymore use any expression. With eh <> syntax you can also do this:

def foo(<type_for(:foo, 1)> bar)
def foo(<foo bar> qux)

That's true -- maybe allow both? When the delimiters are used, the content within is evaluated, and the returned symbol/class will be used. If we decide to use symbols (given that this actually gets implemented...) the return value should just respond to #to_sym.

   def foo(str bar, <"i" + "nt"> baz); end

Daniel

Hi David,

dblack@wobblini.net wrote:

I think I'm not understanding what you mean by the context of an
object. Your example involves two different objects (I assume), so
I'm not clear on which object's context/type you're referring to. Or
do you mean the context in which a given method name appears?

"Context" here is the knowledge that certain methods are not only defined, but that they have certain semantics -- i.e. Can#open and Connection#open have the same name, but they do different things. If an object were both a can and a connection (but not necessarily instances of Can or Connection), it could define #to_can and #to_conn methods. Then any method requiring a can could call #to_can on the argument, and expect the #open method on the object returned to behave as expected. #to_can could of course return a Can instance, but I just don't see the reason why that should be the only option; that would mean that all cans must inherit from Can, which in this case of course is reasonable, but it isn't always. If we require that the object be of a certain class, we make inheritance about type, and not just behavior. That's why I think we should (where it's not absolutely necessary) disregard the class of the return value of the #to_* methods, and instead just expect it to behave properly.

str = "foo"
str.object_id #=> 23456248224800
str.to_str.object_id #=> 23456248224800

Well, at least it's not behaving differently :slight_smile:

Okay, bad example :stuck_out_tongue:

What I mean is: there's already a convention of using to_* for these
class-specific conversion methods, so if you want to create a system
of true type (as opposed to class) conversions, to_* might be a
misleading name.

In other words, I'm not suggesting that how the core to_* methods work
should be a model for how everything should work, but only that
anything that doesn't work the way they work maybe shouldn't be called
to_*.

You do have a very good point there -- the #to_* methods have been used in a rather non-quacky way. Perhaps #as_*?

Basically, I think of objects as pieces of information. This information can have several different representations, but the type should always be about the information, not the representation.

   class DollarAmount
     def initialize(amount)
       @amount = amount.to_f
     end
     def to_str
       "$#{@amount}"
     end
     def to_f
       @amount
     end
   end

Here the information is both the amount of dough, *and* the fact that it's in dollars, and not yen. There a two other representations of the instances of the class, a string and a float -- these representations contain less information than the original object would, but they can be manipulated in ways a DollarAmount can't, unless you define lots and lots of methods yourself.

The reason I think we should use the #to_* methods on received arguments (I'm not sure whether or not you agree that it's a good idea) is that I believe it to be the responsibility of a method to decide which representation of an argument it wants. One method may convert the DollarAmount to euro, so it needs the numeric representation -- but it should be smart enough to be able to receive a DollarAmount instance, and get the relevant representation itself.

   def dollar_to_euro(dollars)
     dollars.to_f * D2E
   end

The reason I think we should disregard the class of the objects returned by #to_* is that it decreases the flexibility of Ruby -- sometimes you want to create a completely different implementation of a "type", like, say, a hash, but with the exact same interface. An OrderedHash, perhaps. Should the author then be forced to have his class inherit from Hash, when really the implementation is completely different? #to_hash should return a hash, but must it be a Hash?

Daniel

Actually, the classes aren't handling the conversion - it's just calling
the standard coercion methods (String(), Integer(), etc). So you'd
probably have to define a BadgerMilk() method. Failing that, you just
pass lowercase names to get the (less strict) to_whatever conversion,
handled as you say by the arguments themselves, i.e. you could also do a
to_badgermilk where it made sense, and call argsto(:badgermilk) ).

Anyway, I did say it was a naive implementation ... :slight_smile:

···

On Sun, 2006-05-28 at 19:58 +0900, Daniel Schierbeck wrote:

Ross Bamford wrote:
> On Sun, 2006-05-28 at 19:37 +0900, Daniel Schierbeck wrote:
>> That's very interesting. Would it be better if class objects were given
>> instead of class names? Otherwise it could be tedious to convert to
>> A::b::BadgerMilk :slight_smile:
>
> Good point :slight_smile: The way it's written, you can do that already, since the
> 'syms' are all to_s'd anyway.
>
> $intclz = Integer # just for example
>
> def ameth(foo, bar, baz, &blk)
> argsto($intclz, String, :sym)
> blk.call(foo, bar, baz)
> end

I don't think that'll work with "nested" constants, i.e. A::b::C. I
think it may complicate things too much to allow classes/class names --
besides, it should be the arguments, not the classes, that should handle
the conversion.

--
Ross Bamford - rosco@roscopeco.REMOVE.co.uk

Okay, I've implemented it slightly different:

   require 'facet/binding'

   def args_to(types = {})
     Binding.of_caller do |binding|
       types.to_hash.each do |arg, type|
         binding[arg] = binding[arg].send("to_#{type}")
       end
     end
   end

   def test(foo, bar, baz)
     args_to :foo => :sym, :baz => :s
     return foo, bar, baz
   end

   p test("foo", "bar", 42) #=> [:foo, "bar", "42"]

Daniel

  # Built-in adaption routes that Ruby already uses in its C code.
  adaption :to => Symbol, :via => :to_sym
  adaption :to => String, :via => :to_str
  adaption :to => Array, :via => :to_ary
  adaption :to => Integer, :via => :to_int

That's interesting. Is this data accessable via pure Ruby code? I was
playing with a lib that provided these like so:

  class Symbol
    def self.coercer ; :to_sym ; end
    def self.convertor ; :intern ; end
  end

  class Array
    def self.coercer ; :to_ary ; end
    def self.convertor ; :to_a ; end
  end

  class Hash
    def self.coercer ; :to_hash ; end
    def self.convertor ; :to_h ; end
  end

  class String
    def self.coercer ; :to_str ; end
    def self.convertor ; :to_s ; end
  end

Etc. that way any class could provide the information.

T.

Ross Bamford wrote:

···

On Sun, 2006-05-28 at 19:58 +0900, Daniel Schierbeck wrote:

Ross Bamford wrote:

On Sun, 2006-05-28 at 19:37 +0900, Daniel Schierbeck wrote:

That's very interesting. Would it be better if class objects were given instead of class names? Otherwise it could be tedious to convert to A::b::BadgerMilk :slight_smile:

Good point :slight_smile: The way it's written, you can do that already, since the
'syms' are all to_s'd anyway.

$intclz = Integer # just for example

def ameth(foo, bar, baz, &blk)
  argsto($intclz, String, :sym)
  blk.call(foo, bar, baz)
end

I don't think that'll work with "nested" constants, i.e. A::b::C. I think it may complicate things too much to allow classes/class names -- besides, it should be the arguments, not the classes, that should handle the conversion.

Actually, the classes aren't handling the conversion - it's just calling
the standard coercion methods (String(), Integer(), etc). So you'd
probably have to define a BadgerMilk() method. Failing that, you just
pass lowercase names to get the (less strict) to_whatever conversion,
handled as you say by the arguments themselves, i.e. you could also do a
to_badgermilk where it made sense, and call argsto(:badgermilk) ).

Anyway, I did say it was a naive implementation ... :slight_smile:

I know; but it's always fun to deconstruct an implementation :slight_smile:

transfire@gmail.com wrote:

  # Built-in adaption routes that Ruby already uses in its C code.
  adaption :to => Symbol, :via => :to_sym
  adaption :to => String, :via => :to_str
  adaption :to => Array, :via => :to_ary
  adaption :to => Integer, :via => :to_int

That's interesting. Is this data accessable via pure Ruby code? I was
playing with a lib that provided these like so:

  class Symbol
    def self.coercer ; :to_sym ; end
    def self.convertor ; :intern ; end
  end

Etc. that way any class could provide the information.

ruby-contract's API for providing adaptions is pretty simple. adaption() also supports more powerful routes. See http://ruby-contract.rubyforge.org/doc/classes/Kernel.html#M000019

You can also see the unit tests at http://ruby-contract.rubyforge.org/release/ruby-contract-0.1.1/test/tc_integration.rb for sample usage.

You can get a list of all adaption routes from the outside by doing Contract.adaptions, but I don't see a case where that would be necessary. For that reason the method is undocumented.

···

--
http://flgr.0x42.net/