jashkenas/coffee-script — Issue #2053: Splitting CS into Language plus Standard Library
Author: alexkg
Created: 2012-01-18 11:42:54+00:00
State: open

# Introduction

This is a kind of "philosophical issue" concerning the direction of CS.

It mainly concerns constructs in CS like `loop`, `class extends`, and if it gets included, [the `extended` hook](https://github.com/jashkenas/coffee-script/pull/1960).

My main problem with the `extended` hook is that it introduces higher-level machinery into the language that should be provided by the program itself. In doing so, CS becomes separate to JS – in addition syntax translation, the language has baked in assumptions about its environment. This "proposal" is about enriching the language at the base level to make it easier to handle common patterns like `class extends` and `extended` outside the language.

# Background

Classes in CS implement a very constrained idea of inheritance. As a purely syntactic construction, trying to implement mix-ins or other extensions gets clunky:

class A extends (mix B, C, D) # `mix` occurs before `extends`, so there's no way
... # of injecting post-extends hooks.

class A extends Base # `@extend` occurs after `extends`, but before
@extend B, C, D # `definition` (unless it's at the end), so there's
... # no way of injecting post-definition hooks.

extend B, C, D, class A # `extend` happens after `extends` and definition
# and can inject post-extends and post-definition hooks.

A = extend B, C, D, -> # might as well just do this...
...

The proposed `extended` hook goes some way towards solving this problem...

However the `extended` hook seems like it's compensating for the deficiencies of `class extends`, of having what should be a library construct baked directly into the language. Since we can't build on `extends` as a function, we have to extend the language itself to get what we want.

Instead of elaborating some kind of class mechanism in CS, I think the emphasis should be on the lower level elements that can support this kind of behaviour in a library.

# What does the current system provide?

## Named functions:

class A extends Drugs
# =>
function A () {}

## Class extension:

function extend( child, parent ) {
// ...
function ctor() { this.constructor = child; }
ctor.prototype = parent.prototype;
child.prototype = new ctor;
child.__super__ = parent.prototype;
return child;
}

## Clean definition syntax:

class A

constructor: ->
@all.push @

@all: []

toString: -> "an A"

## Executable class bodies:

class Logger # *yoink*
if env is 'development'
log: (err) -> console.log err
else if env is 'production'
log: (err) -> console.error err

## And most importantly, super:

class A
toString: -> "an A"

class B extends A
toString: -> "a B (was #{ super })"

# How can this be implemented as a library?

## Named functions:

Reintroducing named functions is [possible, but controversial](https://github.com/jashkenas/coffee-script/issues/15), but essentially non-essential.

## Class extension:

The `extends` functionality is already external to the language.

## Clean definition syntax:

The current syntax is certainly neat, but nothing that couldn't be arrived at in another way:

class A
@classVar = []
@::protoVar = ->

## Executable class bodies:

As extends can be written as a function, we can just use a function for our definition:

A = extend B, ->
@classVar = []
@::protoVar = ->

## Using objects as definitions:

Using object literals instead of objects can make things a bit cleaner:

A = extend B
@classVar = []
protoVar: ->

It just means out prototype variables have to be last.

## The super reference:

This is the most difficult part, since it's hard to figure out if a given function is class-bound, in which case we want `this.__super__`, or if it's prototype bound, in which case we want `this.constructor.__super__`.

Short of specifying exactly which one, we could just use the `@::` syntax above to distinguish prototype and class functions.

# What features could be added to support class-extends?

## Executable object literals

If object literals themselves were translated from:

a: 1, b: 2

To:

new function () {
this.a = 1;
this.b = 2;
}

Then we'd have a bit more flexibility in how we define our class:

(Here, `extend` executes the definition on the prototype.)

A = extend B, ->
protoVar1: ->
@@classVar = []
protoVar2: ->

Or even:

A = extend B, ->
@constructor: ->
@@store.push @
@@store = []

This also presumes the existence of a `@@` operator meaning `this.constructor`.

## Mustache

If the [chaining vs cascading syntax](https://github.com/jashkenas/coffee-script/issues/1495) gets nailed, we wouldn't even need abstraction:

Thing = ->
@createdAt = new Date()
::toString -> "I am a thing."

Would compile to:

Thing = function () {
@createdAt = new Date()
}
Thing.prototype.toString = function () { return "I am a thing." }

# Loop

And after all that, what's wrong with `loop`? It just one more word I'm not allowed to use, just so I can say:

loop
forever

Instead of:

while true
forever