Skip to content

It's back: executable class bodies and local methods #640

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

Closed
StanAngeloff opened this issue Aug 22, 2010 · 32 comments
Closed

It's back: executable class bodies and local methods #640

StanAngeloff opened this issue Aug 22, 2010 · 32 comments

Comments

@StanAngeloff
Copy link
Contributor

I have been thinking about extending the classes in CoffeeScript. Two of the most requested features so far have been executable class bodies and local methods. I have to agree, both provide extensibility and great control. So, without further due, here is what I have in mind:

  • Allow any code within classes, i.e., just like we do for while blocks
  • Require methods to be defined using = and prefixed with a @. The current notion fn: -> makes sense if we think of the class as an object bag, but it's not going to be just that any more
  • Local/private (of sorts) methods are defined without the @

Here is the stuff outlined so far in action:

class Hello
  local = ->
    say 'hello'

  @public = ->
    local()

###

var Hello;
Hello = function() {};
(function() {
  var local;
  local = function() {
    return say('hello');
  };
  this.public = function() {
    return local();
  };
}).call(Hello.prototype);

Nothing too major and no big breaking changes. To build on top of that, let's add some executing code:

property = (target, name) ->
  target[name] = 
    get: -> return target["_#{name}"]
    set: (value) -> target["_#{name}"] = value

class Hello
  property @, 'name'

  @public: ->
    say @name.get()

###

var Hello;
Hello = function() {};
(function() {
  property(this, 'name');
  this.public = function() {
    say(this.name.get());
  };
}).call(Hello.prototype);

The above adds a property to the prototype of Hello and defines two methods on it to act as getters/setters.

But enough about the goodies we get, let's think about side effects and bad design choices we can work over so we can move this baby forward:

  • this is not available in local methods - let's not fuss about this too much. It's JavaScript and we can document it
  • static functions will need @@ or any other combination of symbols to be defined - it's an inconvenience more than an issue, but I am sure we can think of a better way to deal with this. As for referencing local methods inside a static function, I reckon this is not necessary
  • It mixes poorly with our existing syntax - au contraire. The existing syntax is poor since we should really be using fn = -> to define functions. Also @ references an instance everywhere else except within classes where it references the definition, not the reference. The proposed changes address this

My two requests: Do you find executable class bodies useful? If so, what potential issues do you see and how do we fix 'em?

@stephank
Copy link

I can't think of a good way to deal with this in private methods either. Perhaps we should avoid calling them methods altogether, to avoid confusion. They are really just local functions in the class body.

Besides properties, another possible use for this is function decorators. So one might do, for example:

@addFive = (x) ->
  x + 5
@addFive = logCalls(addFive)

I'm actually thinking about doing some basic RPC over WebSockets. Decorating and/or adding properties to functions could be useful for that.

Perhaps to help compressors, rather than call the scope function in the prototype context, simply pass the prototype as a parameter? Would require a bit more effort when parsing @, though.

Finally, Ruby has this odd feature that allows you to reopen a class definition. It seems within grasp, but I'm not sure if it's something you want to support.

@weepy
Copy link

weepy commented Aug 22, 2010

In Kaffeine, I decided to make the class declaration as the function constructor , as that makes it closer to the raw Javascript (and hence executable class bodies). E.g.:

class User(name)
  @name = name
  fn = -> doSetup()
  
::fullName = -> @first + " " + @second
::active = -> !!@lastActivityAt

@stephank
Copy link

A Ruby ActiveRecord-like example that may fit within this proposal:

class Model
  # Method usually used in a class context.
  @has_many = (names) ->
    # Define the accessor method.
    @[names] = ->
      # Retrieve stuff from table 'names'

class Account extends Model
  @has_many 'emails'

Stan made a gist with a possible compiled version of this.

@jashkenas
Copy link
Owner

Ok ... I've played around with a draft of this, on a branch, here:

http://github.com/jashkenas/coffee-script/tree/executable_bodies

If you're interested in it, I encourage you to check out the branch, and play around. Note that the compiler is only partially converted -- only src/scope.coffee uses the executable style, and only that file can be converted with bin/coffee. The rest must still be compiled with CoffeeScript 0.9.2.

I'm not planning on merging it to master unless there are some really compelling demonstrations / examples, but as a proof of concept, it's there -- thanks to Stan's neat prototype-as-the-target-of-the-class-body trick.

@StanAngeloff
Copy link
Contributor Author

I have been trying to come up with examples that can be unique on their own, but unfortunately pretty much everything I have tried can be accomplished in Coffee already. Here is an example with the new syntax:

class Model
  @property = (name) ->
    value = undefined
    @[name] =
      get: -> return value
      set: (newValue) -> value = newValue

class Account extends Model
  @property 'name'
  @property 'amount'

johny = new Account
johny.name.set "Johny's Treasure"
johny.amount.set 100
puts "#{ johny.name.get() } is worth £#{ johny.amount.get() }."

and here is the alternative that is already compiling:

class Model
  @property: (name) ->
    value = undefined
    return {
      get: -> return value
      set: (newValue) -> value = newValue
    }

class Account extends Model
  name:   Model.property()
  amount: Model.property()

johny = new Account
johny.name.set "Johny's Treasure"
johny.amount.set 100
puts "#{ johny.name.get() } is worth £#{ johny.amount.get() }."

I'd much rather have typed @property instead of Model.property. If we take at least one thing from this ticket, how about evaluating all properties using the closure (function() { ... }).call(Account.prototype). This way we would have access to the prototype and any inherited methods can be referenced with @:

class Account extends Model
  name:   @property()
  amount: @property()

On a side note, I am still in favour of using @fn = -> for the assignments as currently this looks as if it's going to work, but there is too much magic happening:

class A
  @hello: -> -> puts 'World!'
  sayHello: @hello()

@michaelficarra
Copy link
Collaborator

Is there still no known way to allow private methods to access the instance through this with executable class bodies? That's the one main problem keeping me from supporting this idea. It's just too unintuitive, even if heavily documented. A user shouldn't need to go to the documentation for things like that.

@weepy
Copy link

weepy commented Aug 25, 2010

I don't understand why you dont make the class body simply the constructor like normal javascript. Doesn't this cover all the use cases ?

@stephank
Copy link

@michaelficarra: Unfortunately, there's no easy way. JavaScript determines what this is at the call site. So the compiler would have to rewrite all call sites, or bind all private methods to the proper this (which only becomes available after instantiation). I'm no expert, but that seems like a lot of effort. :)

@StanAngeloff: The way I see it, it gives us neat syntax for accessing the prototype and doing all sorts of things with it, rather than just assign attributes. So that's the direction I would look in for possible uses.

The has_many example I gave above is fairly simple, but you can see that the has_many implementation could do interesting things like create multiple methods, build a list of metadata across multiple has_many calls, etc. This is not too original, because Ruby on Rails does this.

A simpler example might be a mixin. Currently, you would do something like mixin(myClass, myMixin) outside of the class definition. Or perhaps with a helper on the Function prototype as myClass.mixin(myMixin). With executable bodies, you get sugar: @mixin myMixin.

Another idea I mentioned was RPC. Basic part of that is serialization, which could happen as follows:

class SerializableMixin
  @property = (names..., binaryType) ->
    @_serializable_properties ||= []
    for name in names
      @_serializable_properties.push [name, binaryType]
    return

class Tank
  @mixin SerializableMixin

  @property 'x', 'y' 'uint32'
  @property 'armour', 'uint8'

  @constructor = (@x, @y) ->
    @armour = 255

t = new Tank(240, 480)
data = rpc.serialize(t)

@satyr
Copy link
Collaborator

satyr commented Aug 26, 2010

A simpler, more native approach

class C
  private = on
  @staticMethod = ->
  @prototype =
    initialize: ->
    instanceMethod: ->

into
function C(){ (this.initialize || isNaN).apply(this, arguments); }
(function(){
var private = true;
this.staticMethod = function(){};
this.prototype = {
initialize: function(){},
instanceMethod: function(){}
};
this.prototype.constructor = this;
}).call(C);
Pros : less magic
Cons : deeper indentation for proto members

For the cons, we could use a little magic that appends this.prototype = to an top level object literal:
class C
private = on
@staticmethod = ->
initialize: ->
instanceMethod: ->

@devongovett
Copy link

Hmm. I do see the benefit of executable class bodies, but I'm not sure about the syntax for declaring private and public methods that you propose here. What about class methods that were previously declared using the @ symbol. See my ticket on private methods (#651) for my proposal which would use a similar syntax to what Ruby uses for declaring private methods. Also, I like the colon syntax that classes have now rather than the = sign that you propose here.

@devongovett
Copy link

Also, my proposal (ticket #651) addresses the issue of accessing this within private methods.

@StanAngeloff
Copy link
Contributor Author

I feel like things have been watered-down a bit so back to my main concern:

I am still in favour of using @fn: -> for prototype assignments as currently this looks as if it's going to work, but there is too much magic happening:

class A
  @hello: -> -> puts 'World!'
  sayHello: @hello()

How are we going to address the issue with resolving @hello()? If we ignore the private/public part of this ticket for a moment this still leaves my other suggestion for dealing with the prototype. I am still very much in favour of it as it will make things more consistent, even if we decide not to change the : to =.

@satyr
Copy link
Collaborator

satyr commented Aug 28, 2010

StanAngeloff: How are we going to address the issue with resolving @hello()?

Agreed that this is quite confusing. I'd expect @/this under a class body represents the class itself as in Ruby's self.

using @fn: -> for prototype assignments

...and thus not in favor of this (see my mockup above).

@michaelficarra
Copy link
Collaborator

As a combination of StanAngeloff and Satyr's suggestions, I present my own proposal:

class C
    private = on
    @public = off
    C.staticMethod = -> 0
    @@staticMethod2 = -> 1
    privateMethod = -> 2
    @instanceMethod = -> 3

function C(){ var i=this.initialize; return i ? i.apply(this,arguments) : this; };
(function(){
    var private, privateMethod;
    private = true;
    this.public = false;
    C.staticMethod = function(){ return 0; };
    C.staticMethod2 = function(){ return 1; };
    privateMethod = function(){ return 2; };
    this.instanceMethod = function(){ return 3; };
    return this.constructor = C;
}).call(C.prototype);

Unfortunately, it suffers the same problem that they all do, with private methods unable to access the instance unless explicitly called with the instance as its context. I agree with StanAngeloff that the @ represents an instance everywhere else and should be consistent inside class definitions.

@zmthy
Copy link
Collaborator

zmthy commented Aug 28, 2010

All these syntax proposals have gotten awfully cluttered. Supplying fields for whether values are public or private seems both unnecessary and confusing. Frankly, if a class body is going to be executable (which I think it should be), it needs to appear similar to an ordinary function.

While the current implementation is a 'good enough' approach that appears as a classical OO programmer would expect, because of the way Javascript works they may occasionally be caught out.
Consider this, for instance:
class Class
things: []
This doesn't work in the same way as classical OO, as the array is attached to the prototype and hence each object that uses this prototype has the same array. Adding to it from one object adds it for all of the other objects, whereas in classical OO this syntax would mean a separate array for each object.

It's also important to note that it's practically impossible to implement classic OO classes in Javascript in a manner that doesn't hog memory. An implementation would need to account for the three different types of properties an object can have in Javascript: local to the constructor, public with local access, and public by attachment to the prototype. Each has their place in Javascript, and each should be easy to use, and clear in their use.

As a result, I agree almost wholeheartedly with weepy's suggestion, which makes it very clear where function are being assigned to (due to their prefix) and what they have access to (due to their indentation). As stated above, though, if a class body is going to be runnable then it should appear so.

How about this as a suggestion, then:
Class = (arg1, arg2) ~>
fn = ->
'local'
@fn = ->
'public-local'
::fn = ->
'public'
Which gives us something like this:
var Class = function (arg1, arg2) {
var fn = function () { return 'local'; };
this.fn = function () { return 'public-local' };
};
Class.prototype.fn = function () { return 'public' };
With this syntax it's clear that the result is a function, as expected in Javascript, and it respects the nature of a Javascript 'class' without trying to implement concepts that are foreign to a prototypical system and challenging to reconcile with the fundamentals of the runtime. What I like about it most is actually that it's clear that an assignment has occurred, which isn't so with the current syntax.

@stephank
Copy link

@Tesco: those are some nice examples, but I still have some doubts.

  • How does the :: construct know what to access? Can there be code in between the class and prototype assignment?
  • How would you approach inheritance in your example? I imagine that in most cases people will be using the :: construct in your syntax, because it allows for inheriting attributes.
  • The distinction between public-local and public confused me for a minute. On the other hand, I like how you've pointed out and addressed a flaw in the current syntax as well, where prototype attributes containing data rather than methods might accidentally be shared.

The many syntaxes show the struggle to understand and somehow hide the underlying JavaScript object system.

@jashkenas
Copy link
Owner

Remember, folks, that we didn't have classes for a long time, and the current class system is just simple sugar for generating a class from an object literal full of prototype properties...

Tesco: Your example can be written like this with the current CoffeeScript:

Class = (arg1, arg2) ->
  fn = ->
    'local'
  @fn = ->
    'public-local'

Class::fn = ->
  'public'

The only difference being a regular function def instead of ~>, and filling in the prefix to the prototype accessor...

@zmthy
Copy link
Collaborator

zmthy commented Aug 28, 2010

@stephank: I considered those points before writing the examples.

I originally had the :: function inside the class function, but realised that it was misleading about what the prototype function could access. The result would be that you could not have code in between the :: functions, which would attach to the class function immediately above.
Inheritance would work as it does at the moment, by just grabbing the prototypal elements.
The whole public-local vs public confusion is exactly why we shouldn't sugar something into doing something outside of what it appears to be. They both have their roles, but the language should be clear which one is which.

@jashkenas: Right. The real point of my proposal is simply brevity while overcoming the automatic return problem we had until the current sugar was created. I believe the current class syntax too closely mirrors classic OO style without fully implementing the concepts.

@zmthy
Copy link
Collaborator

zmthy commented Aug 28, 2010

@jashkenas: Don't forget you'd have to return this at the end of that function as well.

@jashkenas
Copy link
Owner

Tesco: got a minute to pop into #coffeescript to talk about it?

@devongovett
Copy link

If we are going to do private methods, we need to do a real implementation not just some half baked solution. That means that those methods must have access to this as users will expect them to have. The only way to do that as far as I can see is for the compiler to keep track of which methods are private and which ones are not and compile calls to private methods to privateMethod.call(this, arg1, arg2...). If this is not feasible, then we shouldn't add support for private methods to the language. It is as black and white as that in my mind.

Maybe we should just stick to executable class bodies for now until someone comes up with a feasible syntax and implementation of private and public methods.

@michaelficarra
Copy link
Collaborator

I have to pretty much completely disagree with devongovett here. Private methods in javascript just don't have access to this, whether natively or through some framework or higher level abstraction. This fact should be understood by all JS developers and accounted for by them manually writing private method calls in the form privateMethod.call(this,arg1,...,argN) when access to the instance is required. I'd like to hear others' opinions on this.

@devongovett
Copy link

@michaelficarra it depends on what you want. If you want classes to act like a JS closure in that they can contain internal properties and values that are not available externally, this solution is just fine. However, if you want true private methods that act like they are a part of the class you are defining (which would only be intuitive), calls to private methods need to be compiled into privateMethod.call(this, arg1,...,argN).

The currently proposed implementation (for private methods, not executable class bodies) can be accomplished with the current CoffeeScript by doing the following:

Test = (->
    private = ->
        puts "this is private"

    class Test
        runprivate: ->
            private()

    return Test
)()

The above example is intuitive because the private function is not inside the class definition itself. Thus, it shouldn't have access to the this object of the class. If the private method was inside the class, however, I would assume that it would have access.

If we are going to do the work to add a nicer syntax to the language to accomplish something that can already be done with the current CoffeeScript, why not go the extra mile and make the implementation work as users will expect it to work?

@StanAngeloff
Copy link
Contributor Author

Let's put private/local methods to rest for a moment (or split them in a separate ticket). Jeremy, any thoughts on the .call(prototype) part of this ticket as well as using @ and possibly =? I am happy to write up a patch (without executable bodies).

EDIT:
@devongovett: I have to agree with michaelficarra. Local class methods are too sweet to ignore just because this is not going to be available. It's debatable what is expected by the user -- if locals are simply methods wrapped in a closure (and that's the proposed resulting JavaScript), it should be clear what is going on. The current working syntax is cool, but ugly.

@yfeldblum
Copy link

Food for thought:

class Animal

    #instance method
    @yelp = ->
        console.log @_name

    #class method
    @@yelp = ->
        console.log "Animal"

    #local
    yelp = (name) ->
        console.log "hello, #{name}!"
    #used in an instance method
    @myYelp = ->
        yelp @name #this yelp refers to the local above

Locals would be class locals, not instance locals. this within a local would refer to the class, in this case, Animal. This means @ in class bodies (not in instance method bodies) would change meaning and this would introduce @@. I believe this is consistent with michaelficarra's sample.

@andreyvit
Copy link

Here's a way to solve “this” problem using my old proposal: stop treating @foo as this.foo, and rather treat @foo as this_in_outer_instance_method.foo.

By this_in_outer_instance_method I mean this in the method that's defined directly inside class Foo declaration.

That is, code like:

class Foo
  func: ->
    alert @foo
    bar = ->
      alert @foo
    @boz.forEach (el) -> alert "#{@foo} - #{el}"

is compiled into something like:

var Foo = function() { return this; };

Foo.prototype.func = function() {
  var __self = this, bar;
  alert(this.foo);
  bar = function() {
    alert(__self.foo);
  };
  this.boz.forEach(function(el) { alert("" + __self.foo + " - " + el); });
};

That way you can have your private functions with access to @foo, even though they don't have this. Also there's a zero chance of accidentally making an error of using -> where you really wanted to have =>.

@michaelficarra
Copy link
Collaborator

@andreyvit: unfortunately, that would restrict access to the real context value in bar. An example:

class Foo
    private = -> @prop
    @prop = true
    @public = ->
        private.call {prop: false}

new Foo().public()

I would expect this code to return false, but your suggestion would make it return true.

@andreyvit
Copy link

@michaelficarra: right, in this case @ no longer means “this”, but rather “that very specific this”. For my own code, it is a good thing.

@Ezku
Copy link

Ezku commented Sep 25, 2010

The current implementation of classes takes away things from the developer that are possible in plain Javascript, and this proposition gives those things back. It's also syntactically sound and converges use cases to work on the same principles (ie.@method = ->, @foo = -> @method()). I'm also not that much opposed to any of the negatives brought up so far - I'm definitely favoring this.

@StanAngeloff
Copy link
Contributor Author

Wanted to express my position again -- I am happy to take a stab at the grammar/core to allow for the case I've initially proposed -- @this assignments and local scope. There doesn't seem to have been any discussion happening here for the last 2 weeks so let's either kill it or give it the green light.

@jashkenas
Copy link
Owner

Then I'll happily put this one to rest.

@this assignments are no good. While superficially sensible, they break consistency with JavaScript. @ is just this, and is subject to the same rules. For example:

obj = {
  number: 101
  value: -> @number
}

The object has a number property, and a function that returns the value of @number. When calling obj.value(), you get 101. This class:

class Thing
  number: 101
  value: -> @number

... should work in an analogous fashion, and indeed does in the current coffeescript.

Local functions also don't belong in class definitions -- if you'd like to create a local function, you can certainly do so above the class, items in the class definition itself should either be attached to the class itself, or to its prototype.

Closing the ticket -- thanks for the nudge, Stan.

@jashkenas
Copy link
Owner

FYI, I've now got a draft of executable class bodies up over here:

https://github.com/jashkenas/coffee-script/issues#issue/841

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