Feature #4910

Classes as factories

Added by Robert Klemme almost 3 years ago. Updated over 1 year ago.

[ruby-core:37244]
Status:Assigned
Priority:Normal
Assignee:Yukihiro Matsumoto
Category:-
Target version:next minor

Description

I suggest to add these two to class Class:

class Class
alias call new

def to_proc(args)
lambda {|
a| new(*args)}
end
end

Then we can use class instances where blocks are needed and can easily use them as factory instances using the general contract of #call (see example attached).

pro.rb Magnifier - Example file with old and new variants (836 Bytes) Robert Klemme, 06/20/2011 08:50 PM

History

#1 Updated by Benoit Daloze almost 3 years ago

Hello,

Robert Klemme wrote:

I suggest to add these two to class Class:

class Class
alias call new

def to_proc(args)
lambda {|
a| new(*args)}
end
end

Did you want to mean:
def to_proc
lambda { |args| new(args) } # or maybe lambda { |args| new(*args) }
end
?

#toproc is called with no arguments (Symbol.instancemethod(:to_proc).arity # => 0).

Then we can use class instances where blocks are needed and can easily use them as factory instances using the general contract of #call (see example attached).

I don't really see the advantage of defining #call, you could use #new instead at line 16.
If you want more flexibility, I believe it is fine to use a block.

But I like Class#to_proc, and it is indeed some kind of factory helper:

Pos = Struct.new :x,:y
[[1,2],[3,4]].map(&Pos) # => [#<struct Pos x=1, y=2>, #<struct Pos x=3, y=4>]
# instead of
[[1,2],[3,4]].map { |x,y| Pos.new(x,y) }

# note neither #to_proc defined as "lambda { |args| new(args) }" nor map(&Pos.method(:new)) would work:
# ([#<struct Pos x=[1, 2], y=nil>,...])

The obvious limitation being the lack of flexibility for common arguments (e.g.: y always the same). You would then have to use an explicit block.

I do not know if it is worth to add it for this specific case, but it can be nice.

I am also unsure if we need factories in Ruby (certainly not like in statically typed languages).

#2 Updated by Robert Klemme almost 3 years ago

Benoit Daloze wrote:

Hello,

Robert Klemme wrote:

I suggest to add these two to class Class:

class Class
alias call new

def to_proc(args)
lambda {|
a| new(*args)}
end
end

Did you want to mean:
def to_proc
lambda { |args| new(args) } # or maybe lambda { |args| new(*args) }
end
?

#toproc is called with no arguments (Symbol.instancemethod(:to_proc).arity # => 0).

No, it was meant exactly as stated. Advantage is that you can provide parameters to #new if needed while mapping the parameterless call of to_proc easily to the parameterless call of Class#new.

Then we can use class instances where blocks are needed and can easily use them as factory instances using the general contract of #call (see example attached).

I don't really see the advantage of defining #call, you could use #new instead at line 16.
If you want more flexibility, I believe it is fine to use a block.

That's the exact point: by aliasing #new to #call we can pass in a lambda OR a class instance. The most general contract would then be '#call'able (i.e. an anonymous callback function) and as a shortcut we can pass in a class instance.

But I like Class#to_proc, and it is indeed some kind of factory helper:

Pos = Struct.new :x,:y
[[1,2],[3,4]].map(&Pos) # => [#<struct Pos x=1, y=2>, #<struct Pos x=3, y=4>]
# instead of
[[1,2],[3,4]].map { |x,y| Pos.new(x,y) }

# note neither #to_proc defined as "lambda { |args| new(args) }" nor map(&Pos.method(:new)) would work:
# ([#<struct Pos x=[1, 2], y=nil>,...])

The obvious limitation being the lack of flexibility for common arguments (e.g.: y always the same). You would then have to use an explicit block.

I do not know if it is worth to add it for this specific case, but it can be nice.

I had considered that case as well and felt it might not be as common as the case where we try to provide arguments. I do not have any statistics though and I hope for others shedding some more light what they deem more useful.

A variant would be

class Class
def to_proc(args)
if args.empty?
lambda {|
a| new(a)}
else
lambda {|
a| new(*args)}
end
end
end

In other words: if arguments are passed to to_proc use them as sole method arguments for #new; if not, use whatever is passed to the proc (which would support your mapping example).

We could probably make things even more complex by appending *a to *args and truncating the list with the arity of #new at the time of invocation of the block (or, more efficient, time of call of to_proc).

I am also unsure if we need factories in Ruby (certainly not like in statically typed languages).

Any class in Ruby is a factory object already with method #new being the factory method.

#3 Updated by Charles Nutter almost 3 years ago

I'm not sure I agree with adding to_proc to Class instances, since it seems questionable that #new is what you'd always want to be called. Dodging that debate for now, there is another way to get the result you seek:

class Foo
def initialize(i)
@i = i
end
end

(1..50).map(&Foo.method(:new))

This is both more explicit and less magic. If there were syntax added to get method objects (without calling #method) it would be even cleaner.

#4 Updated by Robert Klemme almost 3 years ago

Charles Nutter wrote:

I'm not sure I agree with adding to_proc to Class instances, since it seems questionable that #new is what you'd always want to be called.

Hmm, but what else? I think it is a reasonable default.

Dodging that debate for now, there is another way to get the result you seek:

class Foo
def initialize(i)
@i = i
end
end

(1..50).map(&Foo.method(:new))

This is both more explicit and less magic. If there were syntax added to get method objects (without calling #method) it would be even cleaner.

That's true. Though in absence of that syntax I prefer (1..50).map {|i| Foo.new i} over your solution as it is equally explicit and even less magic - could even be shorter to type. :-) Actually only (1..50).map(&Foo) would be an alternative I would consider.

Cheers

#5 Updated by Yusuke Endoh about 2 years ago

  • Status changed from Open to Assigned
  • Assignee set to Yukihiro Matsumoto

#6 Updated by Yusuke Endoh over 1 year ago

  • Target version set to next minor

Also available in: Atom PDF