jashkenas / coffeescript

Unfancy JavaScript
https://coffeescript.org/
MIT License
16.52k stars 1.98k forks source link

Constructor Arguments enhancements (4) #92

Closed josher19 closed 14 years ago

josher19 commented 14 years ago

I have some suggestions for making CoffeeScript Classes even easier to use in a friendly, "Unfancy" way.

1.) Allow a "default" in function args or ?? in expressions as a shortcut. Existence (?) is great, but it can be even better if we can use it like the || operator.

speed default 45
# is equivalent to any of:
speed ??= 45
speed: speed ?? 45
# current syntax:
speed: if speed? then speed else 45

// in javascript, all 4 would become:

   speed = (typeof(speed) !== 'undefined' && speed !== null) ? speed : 45;

which is much smarter way to do it than

speed = speed || 45;
Bug-prone CoffeeScript example:
Horse: name, speed => 
    this.name: name
    this.speed: speed || 45

later: =>
    deadHorse = new Horse("Gluey", 0);
    "This horse has a speed of " + deadHorse.speed  + "!"
     # ... 45!

See http://gist.github.com/274158 for more examples of common JavaScript class parameter shortcut bugs.

Better CoffeeScript example:
Horse: name, speed => 
    this.name: name
    this.speed: if speed? speed else 45
Best, "Unfancy" CoffeeScript example:
Horse: name, speed default 45 => 
    this.name: name
    this.speed: speed

my

2) A quicker way set my class variables would be using "my": my: => this.attr: attr

Horse: name, speed => 
    this.name: name
    this.speed: if speed? speed else 45

becomes a one liner:

Horse: my name, my speed default 45 => 

It would be nice to have in the compiler so it works well with default:

MyClass: my _x default _y => 

    # is more or less equivalent to:
    MyClass: _x => this._x : _x : _x ?? _y
    # _y should not show up in MyClass.arguments 
    # since it is usually a constant default value.

// in javascript becomes:
function MyClass(_x) {
   this["_x"] = _x = (typeof(_x) !== 'undefined' && _x != null) ? _x : _y;
}

// for Horse example: Horse: my name, my speed default 45 =>

function Horse(name, speed) {
     this.name = name;
     this.speed = speed = (typeof(speed) !== 'undefined' && speed !== null) ? speed : 45;
}

mustbe, force

3) One thing that gets newbies and sometimes veteran Javascript programmers is giving the wrong type of argument to a function or constructor. A common example is forgetting to convert user input in a textfield from a String ("42") to a Number (42). A "mustbe" (throw Error) or "force" (try to convert to new Class) clause, which ideally would be checked at compile time (against primitives, anyway) instead of while running, would be great!

# CoffeeScript:
Horse: my name mustbe "string", my speed default 45 force Number => 

// javascript:
function Horse(name, speed) {
   // name mustbe String
   if (! isType(name, "string") ) throw new ArgTypeError(name, "string");
   // my name
   this.name = name;
   // speed default 45
   speed =  (typeof(speed) !== 'undefined' && speed != null) ? speed : 45;
   // speed force Number
   if (! isType(speed, Number) ) speed = new Number(speed) ;
   // my speed
   this.speed = speed;
}

Sample Javascript and tests for ArgTypeError and isType are in this Gist: http://gist.github.com/277078

Example Usage

4) Extended CoffeeScript examples using proposed new syntax additions: // See: http://gist.github.com/gists/274170

Animal: =>
Animal::move: meters =>
  alert(this.name + " moved " + meters + "m.")
Animal::toString => this.name || this.constructor || "ANIMAL";

Veg: =>
Veg::toString => "Veggies"

Mineral: =>

Snake: my name =>
Snake extends Animal
Snake::move: =>
  alert("Slithering...")
  super(5)
Snake::eats: prey mustbe Animal =>
  if prey.speed + 1 > this.speed then 
      alert(prey + " escapes!") 
  else 
      alert([prey, "swallowed by",this.name].join(" "));

Horse: my name, my speed default 45 force Number => 
Horse extends Animal
Horse::move: =>
  alert("Galloping...")
  super(this.speed)
Horse::eats: prey mustbe Veg => alert("chomp chomp");

sam: new Snake("Sammy the Python")
tom: new Horse("Tommy the Palomino")

// sam.move()
// tom.move()

babyspeed: `$ && $('#speed').val();` # grab from user input, forget to convert to Number.

foal: new Horse("Baby Foal", babyspeed || "4")
grass: new Veg()

alert(getType(foal.speed)) # Number

tom.eats(grass)  # chomp
sam.eats(tom)    # tom escapes
sam.eats(foal)   # gulp!

# won't work:
sam.eats(grass)  # error!
tom.eats(sam)    # error!

Note: without the "force Number" clause, the foal unexpectedly escapes:

"4" + 1 > 5
"41" > 5
true

instead of

4 + 1 > 5
5 > 5
false
jashkenas commented 14 years ago

First off, you win the prize for prettiest issue of the week:

Trophy

I'll need more time to give you a more than superficial response, but at first glance:

All that aside, your suggestions go together hand in hand, and would make for an interesting branch of CoffeeScript, if you want to take a stab at implementing it. I'm sure there are plenty of interesting avenues you could take, if you decided to allow predefined helper functions and compile-time type checking.

Thanks for the lovely ticket.

weepy commented 14 years ago

I like the idea for a ?? and ??= operator, since we already have ? and it's a common anti-pattern.

zmthy commented 14 years ago

I agree about the type-checking, it's not really in the spirit of the language. However, as pointed out in the issue, sometimes it is important an argument be of a specific type, and it's a pain having to run conversions on each one. Having a quick way to ensure the arguments are correct at runtime sure would be handy.

weepy commented 14 years ago

it would be neat if the pattern matching could work with the ||= operator. I.e. you could set defaults for inbound arguments with one line:

[a,b,c] ||= [x,y,z]

compiling to

var __a, a, b, c;
__a = [x, y, z];
 a = a ? a : __a[0];
 b = b ? b : __a[1];
 c = c ? c : __a[2];
jashkenas commented 14 years ago

Getting all flavors of assignment to work with pattern matching is going to be a bit of a trick, but conditional assignment based on existence is now on master.

a ?= 5

Compiles to:

var a;
a = (a !== undefined && a !== null) ? a : 5;

Note that we don't have to do the string comparison on typeof === 'undefined' for the existential assignment, because if it's not in scope we declare it at the top.

...

And finally, you can now use the existential operator infix as well:

result: attempt ? give_up 
jashkenas commented 14 years ago

As for syntax that's a shortcut for this., what do y'all think about this:

Person: first, last =>
  :name: first + last

Person::introduce: =>
  print("Hello there, I'm " + :name)
weepy commented 14 years ago

Ah - nicely done with all these jash. I like how the :x mirrors the prototype ::

zmthy commented 14 years ago

I'm unsure about the ambiguity of saving a property on the context. value: this.prop could exist as value: :prop which is very close to value::prop which could technically mean either of those statements, right?

jashkenas commented 14 years ago

Yeah, I think it's too fuzzy as well -- overloads the colon to the breaking point. Closing the ticket...

weepy commented 14 years ago

Using code>@</code wouldn't be fuzzy as it's not used for anything else :-D.

josher19 commented 14 years ago

@jashkenas:

First, Thanks for the Award! I cut & pasted the text from a Gist and it was a real mess until I put it in the Markdown Previewer and fixed it up. From now on, I'm writing my text there first before posting to preview how it looks.

In theory, you could optimize

a ?= 5

To compile to:

var a;
if (typeof a !== 'undefined' && a !== null) a = 5;

or

var a;
if (a != null) a = 5;

and save the unnecessary assignment of:

a = a;

when a is undefined. Last time I checked (a long time ago) IE 6 died on a === undefined , but null == undefined on all browsers (that's == not ===).


I like the idea of the new infix existential operator:

result: attempt ? give_up

But it may look too close to ternary op ?: in Javascript, which is why I suggested ??

I can see JavaScript coders mis-reading the code as:

 result = attempt ? give_up : null;

instead of

 result = (typeof attempt  !== 'undefined' && attempt  !== null) ? attempt : give_up;
josher19 commented 14 years ago

@Tesco Although it may not be in the spirit of the language, sometimes type checking is needed, and that can be a real pain because of some of JavaScript's quirks. Example: instanceof does inhertitance

var tom = new Horse(); tom instanceof Object === true;

but does not do primitive types:

  var str="This is a string"; str instanceof String

is false even though the constructor is true:

var str="This is a string"; str.constructor === String

So something to ease the pain of doing type checking would be great! Maybe I should just make isType into an (optional) external CS or JS library?

@weepy: I agree, an @ would be nice:

Person: first, last =>
  @name: [first, last].join(" ")

Person::introduce: =>
  print("Hello there, I'm " + @name)
jashkenas commented 14 years ago

@josher:

Underscore.js already has a pretty nice isType suite of checks, optimized for speed. If you'd like to contribute to those, I'd be glad to take the patches. They are:

isEqual, isEmpty, isElement, isArray, isArguments, isFunction, isString, isNumber, isDate, isRegExp isNaN, isNull, isUndefined

http://documentcloud.github.com/underscore/