wren-lang / wren

The Wren Programming Language. Wren is a small, fast, class-based concurrent scripting language.
http://wren.io
MIT License
6.9k stars 552 forks source link

Why is "new" a language construct instead of a method? #238

Closed patriciomacadden closed 9 years ago

patriciomacadden commented 9 years ago

Since wren is an object oriented language, shouldn't "new" be a method?

bjorn commented 9 years ago

It is a method, just one that can only be implicitly called after the memory for an instance was allocated. What advantage do you see in making it a regular method?

patriciomacadden commented 9 years ago

I was thinking that MyClass.new is "more object oriented" than new MyClass, which reminds me of PHP :(

Also, new is a keyword, that's why I said "language construct", because it acts like one. Even if it's a method, it gets called. when the new keyword is parsed.

Please, correct me if I'm wrong, I might be missing something.

patriciomacadden commented 9 years ago

By saying

MyClass.new is "more object oriented" than new MyClass

I mean that MyClass.new means that MyClass is an object, and it receives the new message, which makes more sense to me.

bjorn commented 9 years ago

Having MyClass.new would suggest it is a static method, but it isn't if you look at its implementation, where it can set instance members. Also, there is the memory allocation going on in addition to the call to new. So all in all doing MyClass.new looks a little confusing to me since its behavior would be so different from MyClass.foo while looking exactly the same.

Anyway that's just my opinion. I would have preferred to get rid of the new when instantiating entirely, but it's not possible syntax-wise (see #209).

MarcoLizza commented 9 years ago

I mean that MyClass.new means that MyClass is an object, and it receives the new message

I agree with @bjorn.

In a strict object-oriented approach, messages are sent to object instances and not to classes.

Furthermore, new MyClass is a common idiom that should not scare anyone. :wink:

munificent commented 9 years ago

I would also like to get rid of new. I like that it makes things a bit more familiar to people coming from C++/Java/C#/PHP/JS, but it's kind of awkward and inflexible. Unlike almost everything else in Wren, a class can't control how it behaves. (For example, you couldn't make new return a subclass of your class.)

It's also syntactically a bit weird. It's not clear what most users would intuit something like new a.b.c to mean. (I'd have to try and see how other languages parse that.)

The reason it's there is to make the current implementation easier. It's an implementation detail that wormed its way to the surface of the language. Creating an object involves two steps:

  1. Allocate a blob of memory of the right size and with the right class.
  2. Run its constructor body.

The second step works like any other method call. In particular, that means that you have access to this, can call super, etc. For that to work, it must be an instance method on the class being constructed. That in turn means you have to actually have an instance before you can call it. That's why the first step is separate.

Before we've allocated the memory and wired up its class, there's no this to dispatch on. So, say you have a class like:

class Greeter {
  new(name) {
    IO.print("Hi", name)
  }
}

Ideally, the way this would work is that you'd create an instance of it like:

Greeter.new(name)

Under the hood, Wren would have done two things for you:

  1. Created a static method on Greeter named new. It would be created internally but act something roughly like (the angle brackets here mean "not actual Wren syntax"):

    class Greeter {
     static new(name) {
       var obj = <allocate instance of Greeter>
       obj.<initialize>(name)
       return obj
     }
    }
  2. Created an instance method on Greeter for the constructor, like:

    class Greeter {
     <constructor>(name) {
       IO.print("Hi", name)
     }
    }

So it uses some dark magic to allocate an uninitialized instance of the right class. Then it calls a method on that instance to run the constructor body. It forwards along all of the arguments. Then it returns the result of that.

I couldn't come up with a clean, simple way to implement all of the above in the compiler without a lot of code. Or, at least I couldn't in the day or two I spent mulling it.

Adding a new keyword addressed this. Since the compiler can now tell when an object is being created syntactically at the callsite, it's trivial for the compiler to just output a bytecode sequence that calls a static method to create the instance then calls an instance method on the result. The whole code is just:

static void new_(Compiler* compiler, bool allowAssignment)
{
  // Allow a dotted name after 'new'.
  consume(compiler, TOKEN_NAME, "Expect name after 'new'.");
  name(compiler, false);
  while (match(compiler, TOKEN_DOT))
  {
    call(compiler, false);
  }

  // The angle brackets in the name are to ensure users can't call it directly.
  callMethod(compiler, 0, "<instantiate>", 13);

  // Invoke the constructor on the new instance.
  methodCall(compiler, CODE_CALL_0, "new", 3);
}

In particular, note that it doesn't have to deal with forwarding the constructor arguments because the object is allocated before the arguments are pushed onto the stack.

I'll leave this bug open, because I'm definitely open to changing this, though I do think the current syntax and semantics works pretty well.

patriciomacadden commented 9 years ago

@munificent magnificent explanation, thank you!

I'll try to tackle this issue, but don't expect a quick solution, my C skills are not as strong as I would like, and I want to get more involved with wren too :smile:

Also, @MarcoLizza said:

messages are sent to object instances and not classes

Isn't MyClass an object too (I mean object as "everything is an object")? Since you can send messages to it:

\\/"-
 \_/   wren v0.0.0
> class MyClass {}
> IO.print(MyClass.type)
MyClass metaclass

Maybe I'm too conditioned to see the object model as in Ruby, where (almost) everything is an object...

bjorn commented 9 years ago

Hmm, I didn't realize yet that in Wren you can have static and non-static functions with the same signature on the same class. The same isn't true in C++ and Java. In effect, MyClass.new makes sense of course since it calls the static new as opposed to the non-static new that you generally define.

So this approach already works when you call it for example "construct":

class Foo {
  static construct(arg) {
    var f = new Foo
    f.construct(arg)
    return f
  }

  construct(arg) {
    IO.print("Foo constructed with arg ", arg)
  }
}

IO.print(Foo.construct("bar"))

Complexities aside, I think it makes sense. Though, I can't say yet whether I like it better.

munificent commented 9 years ago

Isn't MyClass an object too (I mean object as "everything is an object")?

That's exactly right. A class is an instance of a metaclass. When you define a "static method", you're actually just defining an instance method on the metaclass. Since that's a different class, you can have instance and static methods on the "same class" that don't collide.

patriciomacadden commented 9 years ago

I've tried to tackle this issue. Let me tell you what I did and what's my limitation. Sorry I don't have a diff!

  1. I removed the new keyword by commenting [this line].(https://github.com/munificent/wren/blob/master/src/vm/wren_compiler.c#L611)
  2. I added a primitive method in src/vm/wren_core.c:
DEF_PRIMITIVE(object_class_new)
{
  Value instance = wrenNewInstance(vm, AS_CLASS(args[0]));
  RETURN_VAL(instance);
}

and bound it in here like this:

PRIMITIVE(vm->objectClass->obj.classObj, "new", object_class_new);

At this point, you can instantiate objects by calling new, like this:

class A {
  new {
    IO.print("hey!")
  }
  hi {
    IO.print("hello!")
  }
}

A.new.hi

The problem, is that one would expect this output:

"hey!"
"hello!"

instead of just "hello!"

My question is: how could I call new on the instance? Maybe with wrenGetMethod and then wrenCall?

Anyway, I don't think it's that easy...

Thoughts?

munificent commented 9 years ago

My question is: how could I call new on the instance?

You can't invoke a user-defined method from within the body of a primitive method. (This is the main limitation fibers foist on us, which makes Wren different from a lot of other interpreters.)

Maybe with wrenGetMethod and then wrenCall?

Those are part of the FFI and are mostly intended for external code to call into Wren. From within the VM itself, you'll want to do something lower level.

The approximate answer is that new on Class should be something like this:

class Class {
  new {
    var instance = allocate_()
    instance.new
    return instance
}

This calls an internal allocate_() method on the class to create an uninitialized instance of the class. Then it invokes the new method (i.e. the constructor) on that new instance. Then it returns it.

I think this would more or less work. The problem is handling arguments to this. They would need to get forwarded from the class's new method to the instance's one. I haven't come up with an elegant solution for that yet, which is why we have a new keyword.

munificent commented 9 years ago

I had a moment of inspiration and realized the tricky part of this—forwarding arguments from the constructor to initializer—can actually be solved easily: just replace the class with the new instance on the stack and leave the other argument slots alone.

I have this working now:

https://github.com/munificent/wren/tree/constructor-methods

Feedback is welcome!

munificent commented 9 years ago

...and this is landed now!