Factory function like Array() for your own classes

S

Sean O'Halpin

Hi,

I was quite surprised today to find out that you can define both a
class and a function with the same name and still be able to reference
them both separately.

This makes it possible to define factory functions that work like the
built-in Array() and String() functions, e.g. so that Thing(1,2,3) is
the same as Thing.new(1,2,3).

(I know that Array(4) is different from Array.new(4) but I'm talking
about what it looks like here.)

Here's an example:

class Thing
factory # defaults to name of class
# ...
end

t =3D Thing:)name =3D> "glove", :description =3D> "white")
t =3D Thing :name =3D> "glove", :description =3D> "white"
t =3D Thing() { |x|
x.name =3D "Alice"
}
t =3D Thing :name =3D> "White Rabbit" do |x|
x.description =3D "flustered"
end

Here's the implementation (it really is very simple):

def factory(name =3D self.to_s)
Kernel::eval "
def #{name}(*args, &block)
#{name}.new(*args, &block)
end
"
end

Note that I use eval here rather than define_method because you can't
pass blocks to blocks (yet).

With this function, you can define factory functions for any class, e.g.

factory :SomeOtherClass
soc =3D SomeOtherClass(*args, &block)

Here's a script to test interaction with blocks:

if __FILE__ =3D=3D $0
class Thing
attr_accessor :attrs
factory
def initialize(attrs =3D {}, &block)
@attrs =3D attrs
block.call(self) if block_given?
end
ends

t =3D Thing()
p t

t =3D Thing :name =3D> "glove", :description =3D> "white"
p t

p Thing() { |x|
x.attrs[:name] =3D "Alice"
}

wr =3D Thing :name =3D> "White Rabbit" do |obj|
obj.attrs[:description] =3D "flustered"
end
p wr

# and just to show that the class is still there
t =3D Thing.new:)name =3D> "Carpenter")

p Module.const_get:)Thing)
p method:)Thing)
end

I think this is a very natural idiom which doesn't seem to have any
unwelcome side-effects. I'm curious what the general opinion is. It
seems a cleaner solution than,say, aliasing [] to new (which I used to
do a lot).

So... what do you think?

Regards,

Sean
 
A

Ara.T.Howard

I think this is a very natural idiom which doesn't seem to have any
unwelcome side-effects. I'm curious what the general opinion is. It seems a
cleaner solution than,say, aliasing [] to new (which I used to do a lot).

So... what do you think?

not a bad idea

harp:~ > cat a.rb
Kernel::methods.grep(%r/[A-Z]/).each do |meth|
obj = send meth, 42
puts "#{ meth }(42) => #{ obj.inspect } - #{ obj.class }"
end

harp:~ > ruby a.rb
Integer(42) => 42 - Fixnum
Float(42) => 42.0 - Float
String(42) => "42" - String
Array(42) => [42] - Array

;-)


-a
--
===============================================================================
| email :: ara [dot] t [dot] howard [at] noaa [dot] gov
| phone :: 303.497.6469
| anything that contradicts experience and logic should be abandoned.
| -- h.h. the 14th dalai lama
===============================================================================
 
D

Dave Burt

Sean O'Halpin:
I was quite surprised today to find out that you can define both a
class and a function with the same name and still be able to reference
them both separately.

This makes it possible to define factory functions that work like the
built-in Array() and String() functions, e.g. so that Thing(1,2,3) is
the same as Thing.new(1,2,3).
...
So... what do you think?

In Ruby, so we're told, methods begin with a lower-case letter. Now,
actually, you can have all kinds of funky names:
irb> class Object
irb> define_method("!@#%$") { 43 }
irb> end
=> #<Proc:0x02b46bd0@(irb):16>
irb> send("!@#%$")
43

I think we don't often see these is because methods _should_ begin in
lower-case.

That said, as Ara has pointed out, there are 4 such methods in the core, and
at least one more in the standard library (Rational()). But these methods
are all cast methods - you feed them something that isn't an Array, Integer,
Float, String, Rational, and it gives you either an object of the correct
type or an error.

I suppose that part of your Thing example could be seen as a cast from a
hash to a Thing, but taking a block is a bit of a stretch. Why not just use
..new? Why does new need an alias?

Cheers,
Dave
 
S

Sean O'Halpin

[snip]
Why not just use ..new? Why does new need an alias?

It doesn't ~need~ an alias. It's just quite handy sometimes when you
are creating lots of nested objects to avoid having to type new all
the time. For example, in a report DSL, you could use something like:

r =3D Report(
:header =3D> Header( :title =3D> "My report" ),
:body =3D> Body( :rows =3D> Query:)sql =3D> "SELECT * FROM Supplier")=
)
)

instead of

r =3D Report.new(
:header =3D> Header.new( :title =3D> "My report" ),
:body =3D> Body.new( :rows =3D> Query.new:)sql =3D> "SELECT * FROM Su=
pplier"))
)

Not a big gain, but in my opinion, the .new in the second example is
just adding noise. It's a matter of taste I guess.

Regards,

Sean
 
A

Ara.T.Howard

[snip]
Why not just use ..new? Why does new need an alias?

It doesn't ~need~ an alias. It's just quite handy sometimes when you
are creating lots of nested objects to avoid having to type new all
the time. For example, in a report DSL, you could use something like:

r = Report(
:header => Header( :title => "My report" ),
:body => Body( :rows => Query:)sql => "SELECT * FROM Supplier"))
)

instead of

r = Report.new(
:header => Header.new( :title => "My report" ),
:body => Body.new( :rows => Query.new:)sql => "SELECT * FROM Supplier"))
)

Not a big gain, but in my opinion, the .new in the second example is
just adding noise. It's a matter of taste I guess.

i'm with you - i do it all the time in my own code ;-)

-a
--
===============================================================================
| email :: ara [dot] t [dot] howard [at] noaa [dot] gov
| phone :: 303.497.6469
| anything that contradicts experience and logic should be abandoned.
| -- h.h. the 14th dalai lama
===============================================================================
 
T

Trans

Sean said:
Hi,

I was quite surprised today to find out that you can define both a
class and a function with the same name and still be able to reference
them both separately.

This makes it possible to define factory functions that work like the
built-in Array() and String() functions, e.g. so that Thing(1,2,3) is
the same as Thing.new(1,2,3).

(I know that Array(4) is different from Array.new(4) but I'm talking
about what it looks like here.)

Here's an example:

class Thing
factory # defaults to name of class
# ...
end

t = Thing:)name => "glove", :description => "white")
t = Thing :name => "glove", :description => "white"
t = Thing() { |x|
x.name = "Alice"
}
t = Thing :name => "White Rabbit" do |x|
x.description = "flustered"
end

Here's the implementation (it really is very simple):

def factory(name = self.to_s)
Kernel::eval "
def #{name}(*args, &block)
#{name}.new(*args, &block)
end
"
end

Note that I use eval here rather than define_method because you can't
pass blocks to blocks (yet).

With this function, you can define factory functions for any class, e.g.

factory :SomeOtherClass
soc = SomeOtherClass(*args, &block)

Here's a script to test interaction with blocks:

if __FILE__ == $0
class Thing
attr_accessor :attrs
factory
def initialize(attrs = {}, &block)
@attrs = attrs
block.call(self) if block_given?
end
ends

t = Thing()
p t

t = Thing :name => "glove", :description => "white"
p t

p Thing() { |x|
x.attrs[:name] = "Alice"
}

wr = Thing :name => "White Rabbit" do |obj|
obj.attrs[:description] = "flustered"
end
p wr

# and just to show that the class is still there
t = Thing.new:)name => "Carpenter")

p Module.const_get:)Thing)
p method:)Thing)
end

I think this is a very natural idiom which doesn't seem to have any
unwelcome side-effects. I'm curious what the general opinion is. It
seems a cleaner solution than,say, aliasing [] to new (which I used to
do a lot).

So... what do you think?

Yep. Yep. Yep. I've sugegsted it before. But some people don't like it,
insisting that the ".new" gives some sort of additional clarity. Don't
know, but like your (our) way better --even wish it were built-in.

T.
 
D

Dave Burt

Trans:
Yep. Yep. Yep. I've sugegsted it before. But some people don't like it,
insisting that the ".new" gives some sort of additional clarity. Don't
know, but like your (our) way better --even wish it were built-in.

Like this?

def method_missing(sym, *args, &block)
const = Object.const_get(sym) rescue nil
if const.kind_of?(Module)
const.new(*args, &block)
else
raise NoMethodError("undefined method '#{sym}' for #{self.inspect}")
# OK, it is cool.
end
end

Cheers,
Dave
 
R

Robert Klemme

Dave Burt said:
Trans:

Like this?

def method_missing(sym, *args, &block)
const = Object.const_get(sym) rescue nil
if const.kind_of?(Module)
const.new(*args, &block)
else
raise NoMethodError("undefined method '#{sym}' for
#{self.inspect}") # OK, it is cool.
end
end

I'd prefer

def method_missing(s, *a, &b)
Object.const_get(s).new(*a,&b)
end

Ruby will throw exceptions for any cases that don't work anyway.

Kind regards

robert
 
R

Robert Klemme

Robert Klemme said:
I'd prefer

def method_missing(s, *a, &b)
Object.const_get(s).new(*a,&b)
end

Ruby will throw exceptions for any cases that don't work anyway.

Kind regards

robert

PS: Note also the alternative approach of Array[]:
class Foo
class <<self
alias :[] :new
end
end => nil
Foo[]
=> #<Foo:0x1018b850>

Kind regards

robert
 
S

Sean O'Halpin

PS: Note also the alternative approach of Array[]:

Indeed - I mentioned it above ("aliasing :[] to new") and have been
using it for years (since I saw it in matju's X11 lib). I've always
liked it myself but have recently discovered that others find it a bit
strange. Also, since I've been using Rails, I've started to use :[] as
an alias for :find so I can do things like this:

person =3D Person[:where =3D> "name like 'A%'"]

This seems a more natural fit - i.e. thinking of Person as an extent /
container that you get things out of fits better with the normal use
of :[] as an fetch operator.

I was wondering if the Object() syntax might be a better solution as
an alias for new. It has the precedence of Array(), Integer(), etc.

As Dave says, method names should be lowercase so it wouldn't affect
anyone who's been following the rules!

I wasn't aware that Trans had already suggested this - can anyone
point me to the discussion?

Cheers,

Sean
 
N

nobu.nokada

Hi,

At Sat, 15 Oct 2005 11:00:44 +0900,
Sean O'Halpin wrote in [ruby-talk:160664]:
Note that I use eval here rather than define_method because you can't
pass blocks to blocks (yet).

class Class
def factory(name = self.to_s)
names = name.split(/::/)
name = names.pop
if names.empty?
mklass = klass = Object
else
klass = names.inject(Object) {|c, n| c.const_get(n)}
mklass = class << klass; self; end
end
meth = klass.const_get(name).method:)new)
mklass.class_eval {define_method(name, &meth)}
end
end
 
G

Gavin Kistner

This makes it possible to define factory functions that work like the
built-in Array() and String() functions, e.g. so that Thing(1,2,3) is
the same as Thing.new(1,2,3).

A language-comparison-aside...

In Lua, every table (the core object unit; basically a Hash with some
extra goodies) can be accessed directly OR called as a function, if
the table's metatable has a __call property that points to a
function. For example:

Person = {
-- constructor
new=function( self, inName, inAge )
local theInstance = { name = inName or "John", age = inAge or 0 }
-- make the instance inherit from the 'class'
setmetatable( theInstance, self._instanceMetaTable )
return theInstance
end,

-- methods and properties common to all instances
prototype = {
species = 'Human',
greet = function( self )
print( "Hello, my name is " .. self.name )
end
}
}

Person._instanceMetaTable = {
__index = Person.prototype,
__tostring = function( self )
return string.format( '<Person name="%s" age="%d">', self.name,
self.age )
end
}

-- Here's the magic that makes the Person table callable as a function
setmetatable( Person, {
__call = function( self, ... )
return self:new( unpack( arg ) )
end
} )

local gk = Person:new( 'Gavin', 32 )
local lk = Person( 'Lisa', 31 )

print( gk, lk )
--> <Person name="Gavin" age="32"> <Person name="Lisa" age="31">

Person.prototype.greet( gk )
--> Hello, my name is Gavin

gk:greet( )
--> Hello, my name is Gavin

lk:greet( )
--> Hello, my name is Lisa

-- Showing that Person isn't a function, just an object with properties
print( Person.prototype )
--> table: 0x303a40
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,579
Members
45,053
Latest member
BrodieSola

Latest Threads

Top