Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Constructors, Prototypes, Inheritance. #17

Closed
madebysofa opened this issue Dec 30, 2009 · 64 comments
Closed

Constructors, Prototypes, Inheritance. #17

madebysofa opened this issue Dec 30, 2009 · 64 comments

Comments

@madebysofa
Copy link

Case one works, case two doesn't. Or am I missing something really obvious here?

http://gist.github.com/266191

@jashkenas
Copy link
Owner

Nice bug.

You're running into a JavaScript (mis)feature -- if you return an object from a constructor, that object will be the evaluated result of new Klass(), not the instance you were trying to make. Because, in CoffeeScript, all functions return their final value, you're getting the array back instead of a Test2. Adding a this as the final line of Test2's constructor fixes the problem.

It would be ideal if CoffeeScript could detect constructor functions, and not add the return statement to their final lines, but unfortunately because any JavaScript function can be used as a constructor, I'm not sure that this is possible. It might be better just to document that this needs to be the final line of a CoffeeScript constructor. Any ideas?

@madebysofa
Copy link
Author

Well to be honest, the sort way returning works now is sort of magic and weird to me. I'm used to do an explicit return and if none was given it returns none or nil. So it seems to me that behavior would be a better fit in combination with javascript wouldn't it?

@jashkenas
Copy link
Owner

I'm not willing to give up implicit returns, just to fix this special case for constructors. They're critical for the whole "everything should be an expression" idea that underlies CoffeeScript.

I guess CoffeeScript could suppress the auto-return for functions that have names starting with a capital letter, CamelCasedClassStyle, assuming that they must be a constructor function, but that would also be quite strange if you aren't expecting it.

@weepy
Copy link

weepy commented Dec 30, 2009

... a case for having a fully-fledged class model, e.g.

class Test1 {
  initializer: => this.test = "test"
  testFunction: => print("It works!")
}

@liamoc
Copy link

liamoc commented Jan 1, 2010

@weepy: One of the reasons I dislike this idea is it is alot of reinvention. JS's prototype model means you can easilly roll mixins and other complicated things into the object system, whereas that sort of Class model makes it needlessly restrictive (i.e like Java)

As for the constructor thing, I don't like enforced naming styles. Can't we just add a "constructor" annotation?

blah = constructor a b c =>
    this.a = a
    this.b = b
    this.c = c

@weepy
Copy link

weepy commented Jan 1, 2010

I didn't mean that it should do anything like Base2, the code i suggested would compile like so :

var Test1 = function Test1() {
  this.initialize()
} 

Test1.prototype = function() {
  this.test = "test"
}

Test1.prototype.testFunction = function() {
  print("It works!")
}

You could take or leave the initialize idea (it's only an idea to make inheritance of the constructor more straight forward).

Another alternative would be to alias the prototype keyword (e.g like jQuery to fn)

@liamoc
Copy link

liamoc commented Jan 1, 2010

Right, I am not suggesting changing semantics, but to fully convey the existing semantic richness of the prototype object system, you will need to come up with tons of new syntax, which I think is completely unneeded.

You're trying to cram Java like semantics into prototypal OO, and I don't like it.

@weepy
Copy link

weepy commented Jan 1, 2010

I don't think I am. Really I'm just trying to figure out a different keyword to "prototype" which is both intimidating and confusing (particularly, to newcomers). It's also a pain to type :D

@jashkenas
Copy link
Owner

If we're going to address this issue, our answer needs to be capable of expressing the full semantic richness of JS's prototype system, as kamatsu says. The problem with exposing all of that plumbing directly is that it's very easy to break or mangle your prototype chain by accident.

I don't think that there's a place for Ruby or Java-esque classes in CoffeeScript. But we should have a convenient syntax for accomplishing the same patterns with prototypes, without the risk of messing up the chain.

extends and super are the start of it, but maybe a more holistic approach is needed, because of this constructor issue, and because it's pretty verbose to have to keep typing Klass.prototype.method:, over and over, as you're defining a superclass (parent object).

@alsonkemp
Copy link

First off, CoffeeScript is lovely. Thanks for spending your time on it.

"You're running into a JavaScript (mis)feature..."
Agreed, but a nice aspect of CoffeeScript is that it is nearly semantically identical to JavaScript. In this case, if a constructor is going to return a value, I also find JavaScript's behavior to be more intuitive than CoffeeScript.

Seems there are at least 3 solutions here:

  1. Break semantic CS/JS semantic equivalence by having the constructor return a value. This is the current behavior. (I find this confusing.)
  2. Kamatsu's suggestion is great, but it also breaks semantic equivalence by having the constructor never return a value. (Better...)
  3. (2++) Adopt Kamatsu's suggestion, but allow functions to use the 'return' statements. 'return' would be implicit in all functions except for 'constructor', so the behavior would be standard CoffeeScript. However, a 'constructor' could 'return' which be equivalent to JavaScript. (Best?)

See http://gist.github.com/267191 (a mod of http://gist.github.com/266191)

@jashkenas
Copy link
Owner

Hey Alson. Thanks for the patch. I have a couple of concerns:

  • Any function can be used as a constructor by writing new func(). So, having to manually tag constructors yourself isn't very foolproof.
  • Even if it was, it's not any easier to tag a function with constructor than it is to just put a this at the bottom of the function, and the latter doesn't have to introduce a new keyword.

So I'm thinking more along the lines of a complete complete class-definition syntax, if we pursue this. You'd have to have a clean way of saying: here is a class, this is its name, these are its methods, these are its properties, this is the constructor.

@jashkenas
Copy link
Owner

A lot of the current verbosity in creating a class lies in repeatedly writing Klass.prototype.method: ... when defining methods. What do y'all think of having a real prototype assignment operator? This:

Bird.fly:: wind => airspeed * wind * effort

Could compile into:

Bird.prototype.fly = function fly(wind) {
  return airspeed * wind * effort;
};

@weepy
Copy link

weepy commented Jan 1, 2010

I'd don't like how it looks like it's a method on Bird itself, prefer sthing like

Bird::fly wind => airspeed * wind * effort

@jashkenas
Copy link
Owner

Yeah, you're right, and it doesn't even save that much. Nevermind.

@jashkenas
Copy link
Owner

Taking the example from the current documentation, what do y'all think of something like this:

class Animal
  constructor: name =>
    this.name: name

  move: meters =>
    alert(this.name + " moved " + meters + "m.")


class Snake extends Animal
  speed: 5

  move: =>
    alert("Slithering...")
    super(this.speed)


class Horse extends Animal
  speed: 45

  move: =>
    alert("Galloping...")
    super(this.speed)

All methods and properties would be defined on the prototype, and the constructor would return "this", naturally.

@weepy
Copy link

weepy commented Jan 1, 2010

How would you go about adding further methods to the prototype. Perhaps Bird::fly isn't so bad after all.

Also, in this example, how would the base constructor get called from the subclasses?

@jashkenas
Copy link
Owner

If you wanted to add methods to the prototype from outside, you can just add them:

Horse.prototype.canter: => ...

You should be able to call your ancestor's constructor using super(), like any other method.

@weepy
Copy link

weepy commented Jan 1, 2010

Sorry - just to be clear, in this instance class Snake extends Animal, the Animal constructor would not be called?

I.e. as with normal JS, the constructor is not subclassed (something that's unfortunately confusing for newcomers).

@jashkenas
Copy link
Owner

Hmm, in the case above, subclasses like Snake should call Animal's constructor unless they choose to override it, no? It might be tricky to implement.

@weepy
Copy link

weepy commented Jan 1, 2010

That's what it should do, but as you say implementation might be tricky. Coffee could keep track of implemented classes, but seems out of the question.

I suggested an implementation where each constructor automatically called an 'initialize' function with the inbound params which would then be subclassed automatically. The only problem I can see with this is that it means that every class would have an initialize function.

@alsonkemp
Copy link

Jeremy,

w.r.t. creating a real Class mechanism: one of the aspects of CoffeeScript I really like is that it's basically just pretty JavaScript. Creating a Class mechanism would add to CoffeeScript something that is not within JavaScript, so I'd be less excited about that.

If you delete the word "Class" from your sample code, wouldn't you have clean CoffeeScript that is concise and doesn't imply anything special?

Animal:
  constructor: name =>
    this.name: name

  move: meters =>
    alert(this.name + " moved " + meters + "m.")

Snake extends Animal:
  speed: 5

  move: =>
    alert("Slithering...")
    super(this.speed)

subclasses like Snake should call Animal's constructor unless 
they choose to override it, no?

From my perspective, Javascript doesn't do that, so I wouldn't expect CoffeeScript to do so.

Alson

@jashkenas
Copy link
Owner

Yeah, I agree completely, which is why there's been so much discussion on this page, and why we haven't moved forward with any of the proposals yet -- I don't think that my most recent posted one is very satisfactory.

Ideally, we can come up with something that keeps JavaScript's prototype flavor (avoiding class), encapsulates the common patterns for setting prototype properties and prototype chains, and provides a greatly shortened syntax. I don't think we've seen that yet -- the field is still wide open.

@alsonkemp
Copy link

One last note: object prototypes already have a constructor member, so 'constructor' is probably a bad term to use (lest people be confused about whether they're modifying widget.prototype.constructor or not). Maybe '_'.

Animal:
  _ : name =>
    this.name: name
  move: meters =>
    alert(this.name + " moved " + meters + "m.")

Snake extends Animal:
  _ : name => super(name)
  speed: 5
  move: =>
    alert("Slithering...")
    super(this.speed)

This syntax is pretty nice and is just sugar. 'Snake extends Animal' is a little complicated because the constructor needs to be pushed up to run before the object is extended...

@liamoc
Copy link

liamoc commented Jan 2, 2010

The way I tend to write objects in JS is perhaps appropriate here:

var animal = function() {
    //mixins added here
    mixin_stuff(this)
    var private_function = function (a,b) {
    }
    var private_variable;

    //constructor goes here

    this.public_function = function () {
        //Note how private stuff is inherited via closure
    }
    this.public_variable = 5;
}

I believe inheritance is a broken model (I am a haskell programmer by trade) so I tend to use mixins exclusively.

Anyway, the only problem faced by my object model in coffee is that I have to add "this" at the end. The model you are proposing is different, and unfortunately doesn't allow me to make private data via closure. That's why I'd rather a smaller change that just prevented the unsightly "this" from appearing at the end.

@weepy
Copy link

weepy commented Jan 2, 2010

I was just playing with the example at the start - it's quite odd, since it works if the return value is a string, but not an array or and object. Quite odd?!

@weepy
Copy link

weepy commented Jan 2, 2010

We could do somethign like:
Use :: or similar for an alias to this., unless it's preceded by a variable name, in which case, it's an alias to prototype.. So without the Class stuff, an example would be :

Animal: name --> 
  ::name = name
  ::move = meters -->
    alert(::name + " moved " + meters + "m.")

Animal::hello = --> alert("hello")

(I was just also experimenting with using = and --> in above)

@weepy
Copy link

weepy commented Jan 2, 2010

or use @ in place of ::, for something more rubyesque.

@alsonkemp
Copy link

I'm getting a bit confused in this thread... It started off simple (constructor behavior is unexpected) and got pretty complicated (I think there are now a number of separate features within this thread). Maybe we could move this discussion off onto a wiki page and do the following:

  • Add some real world, fairly large sample code gists. The gists we're looking at here are so short that the benefits are both minimal and different for each gist.
  • Enumerate the features:
    1. Align constructor behavior with JS's (maybe)
    2. Provide a bit of sugar for long .prototype. sections
    3. Provide a bit of sugar for long this.* sections
    4. Provide a bit of sugar for Classes (?)
    5. ???

Assuming that y'all are okay with that, I've added a wiki page to my repo and made you all collaborators. Feel free to edit to taste, to add example code, etc. See here:
http://wiki.github.com/alsonkemp/coffee-script/cs-issue-17

And shouts out to any Haskellers who're reading...

@weepy
Copy link

weepy commented Jan 2, 2010

sorry alson - here's another idea to add to the melting pot: supress the return value if the function starts with a capital (it would still be possible to explicitly return from there functions).

@weepy
Copy link

weepy commented Jan 3, 2010

yes i agree,construct could also work (avoiding the collision with JS keyword?)

@jashkenas
Copy link
Owner

It's not a problem to collide with JS keywords -- I'm using "extends" and "super", after all. They won't be present in the final JS.

@liamoc
Copy link

liamoc commented Jan 3, 2010

Right, constructor isn't too bad.

@alsonkemp
Copy link

A question for Jeremy: is CoffeeScript a sugaring of Javascript (better syntax; same semantics)? Or is CoffeeScript a fixing/normalization of Javascript (better syntax; some different semantics)?

If it's sugar, then I tend to agree with Kamatsu that 'class' doesn't seem like an appropriate addition.

If it's a clean-up/normalization, then adding 'class' is lovely.

I tend to use a number of existing JS libraries and would love to use them with CoffeeScript. The semi-guarantee of semantic alignment between JS and CS means that I can do so. If that semi-guarantee is at risk, then CoffeeScript is much less useful to me.

@jashkenas
Copy link
Owner

There should be no risk of semantic mis-matching between CoffeeScript and JavaScript. We're not adding anything to the runtime -- no extensions of core objects, no assumptions of special predefined helper functions, as convenient as it might be to have them.

On the other hand, CoffeeScript is only going to provide syntax for "the good parts" of JavaScript. Ideally, you wouldn't be able to write CoffeeScript that ever creates a warning in JSLint, without it being a syntax error in CoffeeScript first. We're not there yet, but it's a nice abstract goal.

We're definitely wandering away from "same semantics" with some of the syntax that compiles into larger chunks of JS: The existence operator ?, extends, array and object comprehensions, and slice and splice literals all come to mind.

Still, anything that you can write in JavaScript, you should be able to write in CoffeeScript -- including integration with JS libraries. If you can't, please file a bug.

@weepy
Copy link

weepy commented Jan 3, 2010

constructor leaves me with a bit of a empty feeling in my stomach. What exactly are we constructing? Whilst kamatsu is right that we're not creating a classical OO class system, I think it needs some kind of name. Maybe class isn't so bad after all (as long as we educate that it's not a classical OO class).

@liamoc
Copy link

liamoc commented Jan 3, 2010

Except class isn't at all what it is. It has nothing but wrong connotations. The C++/Java choice of the "class" keyword is even a misappropriation from type theory that unfortunately permeated the lexicon. Constructor is a good keyword because you are defining a constructor for an object. You are in no way specifying a class.

@alsonkemp
Copy link

Weepy,

What exactly are we constructing?

We're constructing whatever Javascript is constructing with its 'new' operator... 'constructor' is a notion from Javascript so would be good to mirror in CoffeeScript. In Javascript

new Klass();

behaves differently from

Klass();

CoffeeScript does not mirror this behavior so constructing an object in CS is error-prone if you expect CS to have JS's behavior.

Another possible solution to the issue with 'new' behavior is to adopt Javascript's behavior and wrap the function with a 'return this'. Wouldn't be pretty since every function would have to set something like 'coffee_script_last_this = this', then 'k = new Klass()' would compile to:

new Klass();
k = coffee_script_last_this;

Hackety way to ignore Klass's return value... Probably not thread safe...

@weepy
Copy link

weepy commented Jan 3, 2010

fair enough guys - you're probably right. constructor it is ?

@weepy
Copy link

weepy commented Jan 4, 2010

OK. I've updated the gist to use constructor. Can I get feedback please. http://gist.github.com/gists/267997

@liamoc
Copy link

liamoc commented Jan 4, 2010

LGTM!

@alsonkemp
Copy link

Just making sure that we have all of the bases covered (I liked Kamatsu's example code). In functions, we have notations to declare:

  • local variables
  • this variables
  • prototype variables

If we're going to adopt Ruby-ish syntax, then weepy's proposal looks good.

constructor cons : s => 
  priv_storage : s     # var priv_storage = s;
  @pub_storage : s   # this.pub_storage = s;
  @@pro_storage : s  # this.prototype.pro_storage = s;

These notations, suitably modified, should also work when applied to an object:

priv_storage : s      #var priv_storage = s;
obj@pub_storage : s     # obj.pub_storage = s;
obj@@pro_storage : s  # obj.prototype.pro_storage = s;

One issue is that this notation could cause confusion for Ruby programmers since '@' deals more with privacy than with storage location (both priv_storage and @pub_storage are stored locally, but priv_storage is only available while inside the object).

[ARGH... Github's sometimes-Markdown, sometimes-Textile markup is killing me.]

@weepy
Copy link

weepy commented Jan 4, 2010

Alson, I think you've confused the prototypes with constructor's here. I think it's more like:

constructor Obj : s => 
  storage : s     # var storage = s;
  @storage : s   # this.storage = s;
  @@storage : s  # this.constructor.storage = s; // or Obj.storage = s 
storage : s      #var local_storage = s;
Obj@storage : s     # Obj.prototype.storage = s; // mostly useful for instance variables and functions
Obj@@storage : s  # Obj.storage = s; // this is a bit pointless? 

Essentially @ is used for instance variables. I'm not sure whether the @@ syntax is useful enough to include?

@alsonkemp
Copy link

Doh. Never type before having coffee...

I guess I'm a little confused by the syntax, so was hoping to straighten it out a bit. From your sample it appears that @ sometimes refers to this and sometimes to prototype. An alternative might be to use '.' for instance and '@' for prototype. This would be simple sugar and would match up well with JS:

constructor f : =>
  a : 1 # var a =1;
  .b : 2 # this.b = 2;
  that.c : 3 # that.c = 3;
  that@d : 4 # that.prototype.d = 4;
  that..d : 4 # alternative syntax for that.prototype.d = 4;

a : new f()
.d : 5 # error: no 'this' available
a.d : 5 # a.d = 5;
a@d : 6 # a.prototype.d = 5;
a..d : 6 # alternative syntax

would be lovely to get Jeremy's thoughts here..

[hard to do this via the phone browser!]

@jashkenas
Copy link
Owner

Sure, Alson. Sorry to have been so quiet. This is all a little too nebulous to try and get into CoffeeScript 0.2, so I've been trying to get that ready to release before turning back to this issue. It's a pretty epic thread though -- clearly doing JavaScript inheritance correctly is something that everyone has an interest in seeing happen. I'm happy to let folks come to consensus on a reasonable syntax before trying to build the thing.

But still, some opinions:

  • I'm not a big fan of @, especially when nothing is really private in JavaScript (except local variables). Writing out this.property isn't so bad, and is conceptually consistent with everything else in JS.
  • If we need a shorthand to read and set properties on the prototype, I kind of like weepy's proposal: Bird::fly compiles to Bird.prototype.fly. It would also provide quick access to the core prototypes: Object::hasOwnProperty.apply(...)
  • I'm torn about tagging constructors with constructor, if it's not doing any more than suppressing the return value, but I don't see a better alternative. It looks especially alien with constructor SubObj extends ParentObj: => .... Maybe extends should always be on a line of its own.

Gotta run...

@jnicklas
Copy link

jnicklas commented Jan 4, 2010

To me the reason to use @property and not just this.property is the scoping problems inherit in this that I mentioned before. The fact that this can change with the function binding can be extremely annoying. Using jquery, for example:

$('.something').each() => item
  @submit: href => $.post(href)

  item.find('a').click() =>
    href: $(this).attr('href')
    @submit(href)
    this.remove()

Here, there is clearly a semantic difference between @submit and this.remove. @submit works as though it was a locally declared variable, and becomes part of the closure, this is how it should work, imho.

Regarding the issue with nothing being "private", why carry over this notion from Ruby? I see the @ sign as signifying "public" here, as in, "this property is publically accessible for all objects created by this constructor".

@alsonkemp
Copy link

re: @Property: this implies the addition of the uniqueified 'this', right? (this.this_12345 = this)

Jeremy: thanks for stopping by! ;)

'constructor' notation: this seems error prone. If you forget to include 'constructor' then funky and very hard to detect errors occur. (yes, I have totally flip flopped.) I'm a fan of actually implementing 'new' in CS so that it operates the same as in JS. Might be ugly, but it seems like the best thing to do...

'this' and 'prototype' : since CS is just sugar, I'd prefer to let people elide bits that can reasonably be inferred and 'this' and 'prototype' can be inferred in the following:

.prop # this.prop
inst..prop # inst.prototype.prop

Simple to implement and tightens up the code. It includes a slight risk of turning typos into valid statements.

@liamoc
Copy link

liamoc commented Jan 4, 2010

@jashkenas: whoa whoa whoa, did you just say nothing is really private in javascript? Where did you get that notion? behold!

function objectWithPrivateData() {
    var private_data = 5;
    this.method = function() {
          private_data++;
          return private_data*2;
    }
}

var a = new objectWithPrivateData()
print(a.method()) // = 12
print(a.method()) // = 14

@documentcloud
Copy link

Yeah, yeah, local variables are only accessible from within the closure, sure. Maybe it's just because it doesn't feel like a real private variable (but I'm probably just being old fashioned). Note that when you add methods like this, within the constructor, you have to re-create them every time you instantiate a new object (not-so-efficient), and they're not shared with other instances of the class. And yes, there are ways around this, but they're fairly awkward, which is an opportunity for us to address.

@weepy
Copy link

weepy commented Jan 5, 2010

I never meant @ to mean anything about public and private, or to point to _this_12345 idea ( a separate discussion).

It's just an alternative syntax to my :: suggestion, i.e sugar for this. and prototype.

@prop compiles to this.prop
Bird@fly compiles to Bird.prototype.fly

I like it because:

  1. it's sugar and so exposes the 'real' machinery,
  2. it's fairly compact (this. and prototype are typed alot in JS)
  3. has a consistency (@ implies the notion of property of instance).

Lastly I'd like to make the case of @ over . and :: -- It stands out more and doesn't get confused with assignment (:)

@liamoc
Copy link

liamoc commented Jan 5, 2010

@documentcloud: If the constructor adds methods, the efficiency cost is about as low as a hashtable lookup on each method creation. This is not C and we're already talking about awful performance generally for most JS VMs, a variable assignment is the least of your worries.

As for privacy, closures and objects are actually proven equivalent, so your idea of a closed variable being not a "real" private variable is hogwash, you can mathematically reason about either identically.

Also, they are shared with other instances of the prototype (not class), provided that prototype's constructor calls the previous constructor. This provides a convenient mechanism for implementing mixins and a very expressive object system. A true prototype object system such as Potion's would eliminate the out-of-constructor prototype manipulation, which is just a hack that JS adds.

@weepy
Copy link

weepy commented Jan 5, 2010

I'm splitting this ticket out into several ... it's getting too confused.

@jashkenas
Copy link
Owner

Thanks for splitting it weepy, retiring this thread from the open issues...

@jashkenas
Copy link
Owner

As of Issue #210, CoffeeScript has classes, which fixes the constructor / return value trouble.

alangpierce added a commit to alangpierce/coffeescript that referenced this issue Feb 12, 2017
Fixes decaffeinate/decaffeinate#710

The lexer generates fake tokens for interpolated heregexes, and the ending
tokens were being placed where the start (inclusive) and end (inclusive) index
were one past the end of the heregex. This meant that in a case like
`[a ///#{b}///]`, the end tokens of the heregex and also the implicit function
call end were all being placed at the `]`, so the AST location data would say
that the function call ends at the end of the `]`. This caused decaffeinate to
insert the close-paren after the `]`, which is wrong.

To fix, I can just subtract 1 from the position of those ending heregex tokens
so that their end lines up with the end of the heregex itself. This is similar
to previous fixes that changed `OUTDENT` and `CALL_END` tokens so that the end
of the token lines up with the end of the AST node.
This issue was closed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

5 participants