Crazy thought -- Rubyish type conversion?


#1

Now, I’m sure you’ll all hate this, but I just had to post it.

What if optional “type hinting”, or, more accurately, “type conversion”,
was added to the method definition syntax?

def foo(str bar, i baz, sym bur)
# something
end

would then be the same as

def foo(arg1, arg2, arg3)
bar, baz, bur = arg1.to_str, arg2.to_i, arg3.to_sym
end

I know it would save me a lot of typing – plus, it’s still dynamic; it
wouldn’t (necessarily) require the return values of the #to_* calls to
be instances of a certain class.

I know that some of you prefer to just call methods on the received
arguments, and not use the #to_* methods first to convert them to a
certain “type”, but this proposal/idea wouldn’t in any way make that
more difficult than it is today.

Well, it’s just an idea.

(putting on my flame-proof suit)

Daniel


#2

I even got an implementation, although it obviously differs a lot from
the proposed syntax.

class Module
alias_method :define_method, :define_method
def define_method(name, *types, &block)
if types.length > 0
define_method(name) do |*args|
args.each_index do |i|
unless types[i].nil?
args[i] = args[i].send(“to_#{types[i]}”)
end
end
block.call(*args)
end
else
define_method(name, &block)
end
end
end

class Test
define_method(:foo, :sym, nil, :s) do |a, b, c|
p a, b, c
end
end

Test.new.foo(“bar”, 5, 6) #=> :bar, 5, “6”

Cheers,
Daniel


#3

Farrel L. wrote:

It will make catching exception on type ‘conversion’ a bit hard. How
would you handle:

irb(main):001:0> 1.05.to_sym
NoMethodError: undefined method `to_sym’ for 1.05:Float

I’m not sure where you’re getting at – there should be raised either a
NoMethodError or an ArgumentError. I’m not sure what’s best, but I can’t
see what the big deal is.

One thing that might need some though is whether a rescue clause on the
method definition should rescue exceptions raised when an argument
doesn’t respond to the conversion method, i.e.

def test(str foo)
# blah
rescue ArgumentError # or NoMethodError
puts “dude, you gotta give it a string…”
end

Daniel


#4

Just playing out loud…

def foo(bar, baz, bur) % i, str, sym
# something
end

Can this be done already?

def foo(bar, baz, bur)
argsto :i, :str, :sym
# something
end

Otherwise

def foo(*args) # bar, baz, bur
args.mapto :i, :str, :sym
# something
end

T.


#5

It will make catching exception on type ‘conversion’ a bit hard. How
would you handle:

irb(main):001:0> 1.05.to_sym
NoMethodError: undefined method `to_sym’ for 1.05:Float
from (irb):1

Farrel


#6

removed_email_address@domain.invalid wrote:

def foo(bar, baz, bur)
argsto :i, :str, :sym
# something
end

This seems like the most easily achievable way, although I’d still like
to have the types in the argument list.

Daniel


#7

Ross B. wrote:

      b[lv] = send(type, b[lv])
end

ameth(‘10’,100,‘puts’) { |*args| p args }

=> [10, “100”, :puts]

The added bonus you don’t get if you just use to_i and and so on:

ameth(‘10a’,100,‘puts’) { |*args| p args }
-:28:in `ameth’: invalid value for Integer: “10a” (ArgumentError)
from -:32

It’s not particularly smart or fast, though.

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:

Daniel


#8

On Sun, 2006-05-28 at 18:25 +0900, removed_email_address@domain.invalid wrote:

Can this be done already?

def foo(bar, baz, bur)
argsto :i, :str, :sym
# something
end

Here’s a naive implementation:

require ‘facet/binding’

def argsto(*syms)
Binding.of_caller do |b|
lvs = b.local_variables
lvs.each do |lv|
break if syms.empty?
type = syms.shift.to_s
begin
if type =~ /^[A-Z]/
b[lv] = send(type, b[lv])
else
type = “to_#{type}”
b[lv] = b[lv].send(type)
end
rescue NameError
raise ArgumentError,“No coercion #{type} for #{b[lv].inspect}”,
caller[3…-1]
rescue ArgumentError
raise ArgumentError, $!.message, caller[3…-1]
end
end
end
end

If you really need this kind of thing, it seems to work:

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

ameth(‘10’,100,‘puts’) { |*args| p args }

=> [10, “100”, :puts]

The added bonus you don’t get if you just use to_i and and so on:

ameth(‘10a’,100,‘puts’) { |*args| p args }
-:28:in `ameth’: invalid value for Integer: “10a” (ArgumentError)
from -:32

It’s not particularly smart or fast, though.


#9

Ross B. wrote:

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.


#10

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

Here’s a naive implementation:

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

ameth(‘10’,100,‘puts’) { |*args| p args }

=> [10, “100”, :puts]


#11

Daniel S. wrote:

bar, baz, bur = arg1.to_str, arg2.to_i, arg3.to_sym

end

I have implemented this in ruby-contract. Please see

http://ruby-contract.rubyforge.org/


#12

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

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:


#13

Ross B. wrote:

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:


#14

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


#15

Florian Groß wrote:

I have implemented this in ruby-contract. Please see

http://ruby-contract.rubyforge.org/

Nice, although a bit verbose as a general-usage implementation.

Daniel


#16

Daniel S. wrote:

Florian Groß wrote:

I have implemented this in ruby-contract. Please see

http://ruby-contract.rubyforge.org/

Nice, although a bit verbose as a general-usage implementation.

Hm, hard to make it any shorter without changing Ruby itself.

Here’s your original:

def foo(str bar, i baz, sym bur)
# something
end

And here’s how to do it with ruby-contract:

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

Perhaps it is time for writing another source code filter. What do you
think about?

def foo( bar, baz, bur)

That way you could use any expression where the type appears. Method
calls, variables and so on.


#17

Florian Groß wrote:

Daniel S. wrote:
Perhaps it is time for writing another source code filter. What do you
think about?

def foo( bar, baz, bur)

That way you could use any expression where the type appears. Method
calls, variables and so on.

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.

Perhaps this?

def foo( bar, baz, bur); end

Although I still think this is more terse:

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

Daniel


#18

Daniel S. 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( bar, baz, 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( 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.


#19

Florian Groß wrote:

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( 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


#20

Hi –

On Sun, 28 May 2006, Daniel S. wrote:

Now, I’m sure you’ll all hate this, but I just had to post it.

What if optional “type hinting”, or, more accurately, “type conversion”, was
added to the method definition syntax?

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.

I know it would save me a lot of typing – plus, it’s still dynamic; it
wouldn’t (necessarily) require the return values of the #to_* calls to be
instances of a certain class.

I know that some of you prefer to just call methods on the received
arguments, and not use the #to_* methods first to convert them to a certain
“type”, but this proposal/idea wouldn’t in any way make that more difficult
than it is today.

It might make it less common, though – that is, discourage duck
typing and reinforce the concept that what really matters is what
class an object is.

David