Forum: IronRuby Inheritance in IronRuby - possibly a bug or two?

Posted by Charles Strahan (charles-strahan)
on 2010-09-24 09:01
(Received via mailing list)
I have a couple questions about deriving from C# class from IronRuby. 
For
context, here's a code example that I will refer to here in a bit:

========================================

using System;
using System.Reflection;
using IronRuby;
using IronRuby.Runtime;
using Microsoft.Scripting.Hosting.Providers;

namespace Example1
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person(string firstName, string lastName)
        {
            FirstName = firstName;
            LastName = lastName;
        }
    }

    class Program
    {
        private static readonly string _rubyScript = @"

class Person2 < Example1::Person
  def initialize(first, last)
    super(first, last)
  end
end

Person2.new(""Foo"", ""Bar"")

";

        static void Main(string[] args)
        {
            var runtime = Ruby.CreateRuntime();
            var engine = runtime.GetEngine("rb");
            var context =
(RubyContext)HostingHelpers.GetLanguageContext(engine);
            var scope = engine.CreateScope();
            runtime.LoadAssembly(typeof(Program).Assembly);

            engine.Execute(_rubyScript, scope);

            Console.WriteLine(". . .");
            Console.ReadKey(true);
        }
    }
}


========================================


If you run that code, you'll get the following:


InvalidOperationException: can't allocate class `Person2' that derives 
from
type `Example1::Person' with no default constructor; define Person2#new
singleton method instead of Person2#initialize


Is this a bug, or is this intended behavior?  If this is intentional, 
then I
think there's still a different problem: try adding this default 
constructor
and then run the code:

        public Person()
        {
            Console.WriteLine("Uhmmm... what did IronRuby do with
`super(first, last)`?");
        }

So, the `super(first, last)` still get's executed... but what did it do? 
It
obviously didn't forward those arguments to the non-default 
constructor...

So, I think that means we have one, or possibly two, bugs.

Back to the first question: Wouldn't it be possible to determine the 
correct
constructor to invoke based on the arguments, thus avoiding the 
exception? I
would imagine that the generated/emitted subclass could contain all of 
the
same constructors that the base type has, just passing the arguments on 
to
the base class's corresponding constructor (I hope that made sense - 
sorta
tricky to word that correctly).


By the way, how should I specify that I _don't_ want the Ruby code to be
interpreted? I noticed that the debugger broke
in Microsoft.Scripting.Interpreter.Interpreter... maybe that could be 
part
of the problem.

Cheers,
-Charles
Posted by Tomas Matousek (Guest)
on 2010-09-24 09:43
(Received via mailing list)
This behavior is actually by design.

The error message says: "... define Person2#new singleton method instead 
of Person2#initialize" like so:

class Person2 < Example1::Person
  def self.new(first, last)
    super(first, last)
  end
end

person = Person2.new("Foo", "Bar")
p person.first_name, person.last_name

We do pick up the superclass's constructors if you don't specify no 
initialize method, so this works too:

class Person2 < Person
end

person = Person2.new("Foo", "Bar")
p person.first_name, person.last_name

The reason why this doesn't work when you define initialize is to make 
you aware of the fact that your initialize method doesn't do what you 
might expect, that is it doesn't call the constructor of the superclass.

If you define a default ctor in the superclass and you have initialize 
method in the subclass, the constructor gets invoked to create the 
object and initialize is called to initialize it. This patter is kind of 
close to Ruby semantics.

So you basically have 3 options:

-          Do not define new nor initialize => the ctors from base class 
are available for construction of the subclass

-          Define singleton new => you can choose which base ctor is 
called.

-          The base class has a default ctor and the subclass defines 
initialize method => the default ctor is always used for object 
construction and the initialize is called with the arguments given to 
"new".

The reason why we chose this design is due to difference between Ruby 
and CLR initialization semantics. CLR classes don't separate allocation 
("allocate") from initialization ("initialize") like Ruby does. CLR has 
just constructors (which kind of corresponds to Ruby factory method 
"new"). Constructors combine allocation and initialization. The problem 
with mapping initialize to CLR constructors is that it operates on 
"self" that is already allocated before you can do anything (like call 
super):

  def initialize(first, last)
   p self.first_name                 # self is already an instance of 
Person2 here, so we must have called some constructor already (the 
default one if available)
    super(first, last)                   # what should this do? We can't 
call the constructor again... it's too late.
  end


As for what super(first, last) does in your code ... it calls 
"Object#initialize", which in Ruby 1.9.2 has *args parameters and does 
nothing:

>>> class X; end
=> nil
>>> init = X.instance_method(:initialize)
=> #<UnboundMethod: X(Object)#initialize>
>>> init.parameters
=> [[:rest]]
>>> X.new.send(:initialize, 1,2,3,4,5)
=> #<X:0x0000056>

Tomas

From: ironruby-core-bounces@rubyforge.org 
[mailto:ironruby-core-bounces@rubyforge.org] On Behalf Of Charles 
Strahan
Sent: Thursday, September 23, 2010 11:40 PM
To: ironruby-core@rubyforge.org
Subject: [Ironruby-core] Inheritance in IronRuby - possibly a bug or 
two?

I have a couple questions about deriving from C# class from IronRuby. 
For context, here's a code example that I will refer to here in a bit:

========================================

using System;
using System.Reflection;
using IronRuby;
using IronRuby.Runtime;
using Microsoft.Scripting.Hosting.Providers;

namespace Example1
{
    public class Person
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }

        public Person(string firstName, string lastName)
        {
            FirstName = firstName;
            LastName = lastName;
        }
    }

    class Program
    {
        private static readonly string _rubyScript = @"

class Person2 < Example1::Person
  def initialize(first, last)
    super(first, last)
  end
end

Person2.new(""Foo"", ""Bar"")

";

        static void Main(string[] args)
        {
            var runtime = Ruby.CreateRuntime();
            var engine = runtime.GetEngine("rb");
            var context = 
(RubyContext)HostingHelpers.GetLanguageContext(engine);
            var scope = engine.CreateScope();
            runtime.LoadAssembly(typeof(Program).Assembly);

            engine.Execute(_rubyScript, scope);

            Console.WriteLine(". . .");
            Console.ReadKey(true);
        }
    }
}


========================================


If you run that code, you'll get the following:

InvalidOperationException: can't allocate class `Person2' that derives 
from type `Example1::Person' with no default constructor; define 
Person2#new singleton method instead of Person2#initialize

Is this a bug, or is this intended behavior?  If this is intentional, 
then I think there's still a different problem: try adding this default 
constructor and then run the code:

        public Person()
        {
            Console.WriteLine("Uhmmm... what did IronRuby do with 
`super(first, last)`?");
        }

So, the `super(first, last)` still get's executed... but what did it do? 
It obviously didn't forward those arguments to the non-default 
constructor...

So, I think that means we have one, or possibly two, bugs.

Back to the first question: Wouldn't it be possible to determine the 
correct constructor to invoke based on the arguments, thus avoiding the 
exception? I would imagine that the generated/emitted subclass could 
contain all of the same constructors that the base type has, just 
passing the arguments on to the base class's corresponding constructor 
(I hope that made sense - sorta tricky to word that correctly).


By the way, how should I specify that I _don't_ want the Ruby code to be 
interpreted? I noticed that the debugger broke in 
Microsoft.Scripting.Interpreter.Interpreter... maybe that could be part 
of the problem.

Cheers,
-Charles
Posted by Charles Strahan (charles-strahan)
on 2010-09-24 11:46
(Received via mailing list)
Ah, the semantics do make sense now.

In that case, perhaps there's one more solution:

class PersonShim < Example1::Person
  def self.new(first, last)
    Example1::Person.new(first, last)
  end
end

class Person2 < PersonShim
  def initialize(first, last)
    super(first, last)
  end
end

Person2.new(""Foo"", ""Bar"")



In my case, I need to make sure that the C# types that I expose to 
IronRuby
behave almost identically to what you would expect with a pure Ruby
implementation. If you remember back when I was talking about my game 
engine
clone, I need to make sure that I don't break user's scripts - so I need 
to
preserve the "expected" semantics of `initialize`. What I could do is 
end my
C# classes with "Impl", and write a corresponding Ruby class that 
invokes
the correct constructor in `new` (ex: SpriteSubclass -> Sprite ->
SpriteImpl). Mind giving me a sanity check on this, Tomas?

I think this should fix the problem I was having with subclassing my 
Sprite
class - which get's me one step closer to releasing this beast!


Thank you, Tomas, that was a very thorough explanation.

-Charles


On Fri, Sep 24, 2010 at 2:38 AM, Tomas Matousek <
Please log in before posting. Registration is free and takes only a minute.
Existing account (Switch to SSL-encrypted connection)
NEW: Do you have a Google/GoogleMail or Yahoo account? No registration required!
Log in with Google account | Log in with Yahoo account
No account? Register here.