Coffeescript essence

43
COFFEE SCRIPT Rescuing JS from the accidents of its birth @timruffles truffles.me.uk

Transcript of Coffeescript essence

COFFEE SCRIPT Rescuing JS from the accidents of its birth

@timrufflestruffles.me.uk

ESSENCE VS ACCIDENT

ESSENCE

ACCIDENT

ESSENCE:

var BankAccount = function(balance) { return function(cmd,amount) { amount || (amount = 0); if(cmd == "withdraw") return BankAccount(balance - amount) else if(cmd == "deposit") return BankAccount(balance + amount) else if(cmd == "balance") return balance.toFixed(2) else return "What kind of person would try to " + cmd + " a bank account?" }};

var BankAccount = function(balance) { balance || (balance = 0); return function(cmd,amount) { amount || (amount = 0); if(cmd === "withdraw") return BankAccount(balance - amount) else if(cmd === "deposit") return BankAccount(balance + amount) else if(cmd === "balance") return balance.toFixed(2) else return "What kind of person would try to " + cmd + " a bank account?" }};

λ

ACCIDENT

var BankAccount = function(balance) { balance || (balance = 0); return function(cmd,amount) { amount || (amount = 0); if(cmd === "withdraw") return BankAccount(balance - amount); else if(cmd === "deposit") return BankAccount(balance + amount); else if(cmd === "balance") return balance.toFixed(2); else return "What kind of person would try to " + cmd + " a bank account?"; }};

ESSENCE

BankAccount = (balance = 0) -> (cmd, amount = 0) -> if cmd == "withdraw" then BankAccount balance - amount else if cmd == "deposit" then BankAccount balance + amount else if cmd == "balance" then balance.toFixed 2 else "What kind of person would try to #{cmd} a bank account?"

I’m sorry for picking so long a keyword

Brendan Eich, Javascript’s Creator

It’s nice when what you are saying is clear

var winners = function(race) { race || (race = "today's race"); return "Results of " + race ":\n" + [].slice.call(arguments,1).join("\n");};

var keywordArgs = function(args) { foo = args.foo; bar = args.bar; };

winners = (race = "today's race",winners...) -> "Results of #{race}:\n#{ winners.join("\n") }"

keywordArgs = ({foo,bar}) ->

The Zen of Python, PEP: 20

Beautiful is better than ugly.

Explicit is better than implicit.

Simple is better than complex.

Readability counts.

There should be one obvious way to do it.

‘Idioms’ are often just missing language features

// null guardpossiblyNull && possiblyNull.foo;

// chained comparisonvar valid = foo > 5 && foo < 10;

wasDisplayed = this.displayed;this.displayed = this.hidden;this.hidden = wasDisplayed;

possiblyNull?.foo

valid = 5 < foo < 10

[@hidden,@displayed] = [@displayed,@hidden]

for (var key in anObject) { if(anObject.hasOwnProperty(key) { value = anObject[key]; console.log(key, value); }}

for own key, value of anObject console.log key, value

var transformed = [];for (var index=0, length = listOfThings.length; index < length; index++) { transformed.push(transform(listOfThings[index]));};

var squared = list.map(function(item) { return item * item; });

transformed = (transform item for item in listOfThings)

squared = (item * item for item in list)

squared = list.map (item) -> item * item

var Widget = function(el) { this.sayHi = function() { this.el.show() }; this.el = el; this.el.click(this.sayHi.bind(this));};

var Widget = function(el) { self = this; this.sayHi = function() { self.el.show() }; this.el = el; this.el.click(this.sayHi);};

Widget = (@el) -> @sayHi = => @el.show() @el.click @sayHi

class Widget constructor: (@el) -> @el.click @sayHi sayHi: => @el.show()

It’s worse when nobody can agree on them

var Animal = Actor.extend({ initialize: ... override: function() { this.__super__.override.apply(this,arguments) }});_.extend Animal.prototype, ai.instinctive;

dojo.provide("my.actors.Animal");dojo.declare("my.actors.Animal",[Actor,ai.instinctive],{ constructor: ... override: function() { this.inherited(arguments); }});

var Animal = new JS.Class(Actor,{ init: ... override: function() { this.callSuper(arguments) }});Animal.include(ai.instinctive);

You can do anything with function in JS, but you shouldn’t have to.

Brendan Eich

class Animal extends SuperClass constructor: -> override: -> super _.extend Animal::, ai.instinctive

No mixins/traits

Why would you want == to mean...?

"" == "0" // false0 == "" // true0 == "0" // truefalse == "false" // falsefalse == "0" // truefalse == undefined // falsefalse == null // falsenull == undefined // true" \t\r\n" == 0 // true

http://bonsaiden.github.com/JavaScript-Garden/

var values = { foo: 1, bar: 2,};values.foo = 3;

var maxSize = 2.pow(8);

var alerter = function(value) { alert(value);}(5).times(function() { alerter("hello")});

var greeting = "hello"(10).times(function() { alert(greeting)});

HALTING PROBLEMS

EXPERIENCE

+-----------------------------------------------------+--------------+-------------------+----------+---------------+| File | Coffee Lines | Coffee Characters | JS Lines | JS Characters |+-----------------------------------------------------+--------------+-------------------+----------+---------------+| app/coffee/plv/accessors.coffee | 17 | 320 | x 1.9 | x 1.8 || app/coffee/plv/clock.coffee | 30 | 671 | x 1.6 | x 1.6 || app/coffee/plv/collection.coffee | 38 | 1089 | x 1.9 | x 1.4 || app/coffee/plv/comparison.coffee | 13 | 192 | x 1.5 | x 1.6 || app/coffee/plv/config.coffee | 24 | 597 | x 1.3 | x 1.1 || app/coffee/plv/console.coffee | 8 | 162 | x 1.9 | x 1.7 || app/coffee/plv/controller.coffee | 1 | 41 | x 3.0 | x 1.5 || app/coffee/plv/controllers/main.coffee | 12 | 371 | x 1.8 | x 1.2 || app/coffee/plv/ext/backbone.coffee | 16 | 514 | x 1.9 | x 1.6 || app/coffee/plv/ext/class.coffee | 10 | 288 | x 3.3 | x 2.0 || app/coffee/plv/ext/core.coffee | 126 | 2178 | x 1.7 | x 1.6 || app/coffee/plv/ext/date.coffee | 57 | 1206 | x 1.8 | x 1.5 || app/coffee/plv/layers/core.coffee | 1 | 67 | x 1.0 | x 1.0 || app/coffee/plv/model.coffee | 115 | 3194 | x 1.9 | x 1.5 |....| app/coffee/plv/models/entry_handler.coffee | 10 | 302 | x 1.9 | x 1.2 || app/coffee/plv/models/event.coffee | 15 | 399 | x 1.8 | x 1.6 || app/coffee/plv/models/football/point_in_game.coffee | 113 | 2262 | x 1.5 | x 1.4 || app/coffee/plv/models/football.coffee | 35 | 1272 | x 1.9 | x 1.4 || app/coffee/plv/models/game.coffee | 2 | 71 | x 1.5 | x 1.3 |...| app/coffee/plv/polling.coffee | 30 | 560 | x 1.9 | x 1.8 || app/coffee/plv/procs/boot.coffee | 40 | 1000 | x 1.3 | x 1.1 || app/coffee/plv/share.coffee | 15 | 702 | x 2.1 | x 1.2 || app/coffee/plv/spec/factory.coffee | 194 | 4686 | x 1.1 | x 1.2 || app/coffee/plv/spec/fake_game.coffee | 211 | 5742 | x 1.7 | x 1.2 || app/coffee/plv/spec/fake_server.coffee | 44 | 2360 | x 2.2 | x 1.3 || app/coffee/plv/sync.coffee | 12 | 352 | x 2.0 | x 1.3 || app/coffee/plv/ui.coffee | 42 | 903 | x 1.6 | x 1.7 || app/coffee/plv/view.coffee | 9 | 299 | x 1.6 | x 1.3 || app/coffee/plv/views/avatar_picker.coffee | 12 | 385 | x 1.6 | x 1.4 || app/coffee/plv/views/backup_players.coffee | 5 | 142 | x 1.6 | x 1.3 || app/coffee/plv/views/backup_toggle.coffee | 25 | 729 | x 1.4 | x 1.6 || app/coffee/plv/views/big_game.coffee | 71 | 2623 | x 1.3 | x 1.3 |...| app/coffee/plv/views/loading.coffee | 42 | 1067 | x 1.0 | x 1.3 || app/coffee/plv/views/main.coffee | 336 | 9290 | x 1.3 | x 1.3 || app/coffee/plv/views/message.coffee | 39 | 954 | x 1.8 | x 1.6 || app/coffee/plv/views/nav.coffee | 44 | 931 | x 1.4 | x 1.3 |....| app/coffee/plv/views/sounds.coffee | 85 | 2239 | x 1.5 | x 1.4 || app/coffee/plv/views/standings.coffee | 78 | 2501 | x 1.5 | x 1.4 || app/coffee/plv/views/tutorial.coffee | 81 | 2817 | x 1.7 | x 1.4 || app/coffee/plv/web.coffee | 18 | 461 | x 2.7 | x 2.2 |+-----------------------------------------------------+--------------+-------------------+----------+---------------+| Total | 3552 | 98736 | 5563 | 134774 || Diff | | | + 2011 | + 36038 |+-----------------------------------------------------+--------------+-------------------+----------+---------------+| Ratios JS/Coffee | | | x 1.566 | x 1.365 |+-----------------------------------------------------+--------------+-------------------+----------+---------------+

+------------------+--------------+-------------------+----------+---------------+| | Coffee Lines | Coffee Characters | JS Lines | JS Characters |+------------------+--------------+-------------------+----------+---------------+| Total | 3552 | 98736 | 5563 | 134774 || Diff | | | + 2011 | + 36038 |+------------------+--------------+-------------------+----------+---------------+| Ratios JS/Coffee | | | x 1.566 | x 1.365 |+------------------+--------------+-------------------+----------+---------------+

Looks like Ruby or Python, isn’t a minefield:

more people can write JS!

TOOLS

$ guard$ coffee --watch app/coffee/**/*.coffee

$ js2coffee legacy.js > new_hotness.coffee

HOW DOES IT WORK?

Lexer Rewriter

Grammar

JISON

Parser JSCoffee

( )Tokens Tokens Nodes CodeCode

$ ./bin/coffee -te 'console.log "hello world"'[IDENTIFIER console] [. .] [IDENTIFIER log] [CALL_START (] [STRING "hello world"] [CALL_END )] [TERMINATOR \n]

TOKENS

$ ./bin/coffee -en 'console.log "hello world"'Block Call Value "console" Access "log" Value ""hello world""

NODES

$ ./bin/coffee -epb 'console.log "hello world"'console.log("hello world");

CODE

How about extending coffee script?

Let’s add <=> and user-definable comparison

operators

GOAL

coffee> 300 <=> 500-1

coffee> arrogant = {">": -> true}{ '>': [Function] }

coffee> arrogant > Infinitytrue

coffee> [1,4,2,3,5,6,7].map (val) -> val <=> 5[ -1, -1, -1, -1, 0, 1, 1 ]

LEXER.COFFEE

OPERATOR = /// ^ ( ?: [-=]> # function+ | <=> # our new op | [-+*/%<>&|^!?=]= # compound assign / compare... # Comparison tokens.-COMPARE = ['==', '!=', '<', '>', '<=', '>=']+COMPARE = ['<=>', '==', '!=', '<', '>', '<=', '>=']

GRAMMAR.COFEE

+ isComparison: ->+ @operator in ['<', '>', '>=', '<=', '===', '!==', '<=>'] - code = @first.compile(o, LEVEL_OP) + ' ' + @operator + ' ' + - @second.compile(o, LEVEL_OP)+ code = if @isComparison()+ utility("comparison") + "(" + @first.compile(o, LEVEL_OP) + ", " ++ @second.compile(o, LEVEL_OP) + ", '#{@operator}')"+ else+ @first.compile(o, LEVEL_OP) + ' ' + @operator + ' ' ++ @second.compile(o, LEVEL_OP) if o.level <= LEVEL_OP then code else "(#{code})"

GRAMMAR.COFFEE

+ comparison: -> """+ function(a,b,op) {+ if(!a || !b || !a[op]) {+ switch(op) {+ case "===":+ return a === b;+ case ">=":...+ case "<=>":+ if(a === b) return 0;+ return a > b ? 1 : -1;+ default:+ throw "Unknown operator: " + op;+ }+ } else {+ return a[op](b);+ }+ }+ """

ALL TOGETHER NOW

$ ./bin/coffee -et '5 <=> 6'[NUMBER 5] [COMPARE <=>] [NUMBER 6] [TERMINATOR \n]

$ ./bin/coffee -en '5 <=> 6'Block Op <=> Value "5" Value "6"

$ ./bin/coffee -epb '5 <=> 6'var __comparison = function(a,b,op) { ...};

__comparison(5, 6, '<=>');

coffee> 300 <=> 500-1

coffee> arrogant = {">": -> true}{ '>': [Function] }

coffee> arrogant > Infinitytrue

coffee> [1,4,2,3,5,6,7].map (val) -> val <=> 5[ -1, -1, -1, -1, 0, 1, 1 ]

λ λSame power, 40% less code

Less noise

Less ?!?!

Familiar, clean syntax

No need to wait for IE (took 6.3 years for JS 1.5)

If you fancy it, add your own language features

;{

{foo:bar,}"\n" == 0

Thanks

@timrufflestruffles.me.uk