Chapter 8

Metaprogramming

8.1 Literate CoffeeScript

hello.coffee
###
Log 'Hello world!' to the console
###

console.log 'Hello World!'
hello.litcoffee
Log 'Hello world!' to the console
   
    console.log 'Hello World!'







W B Yeats
The Wild Swans at Coole
The trees are in their autumn beauty,
trees = [{}, {}] for tree in trees
tree.inAutumnBeauty = yes
The woodland paths are dry,
paths = [{}, {}, {}] for path in paths
path.dry = yes
Under the October twilight the water
Mirrors a still sky;
octoberTwilight = {} stillSky = {} water =
placeUnder: ->
water.placeUnder octoberTwilight
water.mirrors = stillSky
Upon the brimming water among the stones
Are nine-and-fifty swans.
water.brimming = true
water.stones = [{}, {}, {}, {}]
class Swan
x: 3
for n in [1..59] water.stones.push new Swan





Listing 8.1 Literate CoffeeScript Rot13

## Rot13                                                      
A simple letter-substitution cipher that replaces a letter    
with the letter 13 letters after it in the alphabet.
charRot13 = (char) ->
The built-in string utility for getting character codes can be used
charCode = char.charCodeAt(0)
If the character is in the alphabet up to 'm', then
add 13 to the character code
charCodeRot13 = if charInRange char, 'a', 'm'
charCode + 13
If the character is after 'm' in the alphabet, then
subtract 13 from the character code
else if charInRange char, 'n', 'z' charCode - 13 else
charCode
Characters can be converted back using the built-in string method
String.fromCharCode charCodeRot13
A character is in a specific range regardless of whether
it's uppercase or lowercase
charInRange = (char, first, last) -> lowerCharCode = char.toLowerCase().charCodeAt(0)
first.charCodeAt(0) <= lowerCharCode <= last.charCodeAt(0)
Converting a string is done by converting all the characters
and joining the results
stringRot13 = (string) -> (charRot13 char for char in string).join ''





8.2 Domain-specific languages

<html>
<p>
It is <strong>very</strong> important that you understand this...
</p>





strongElements = document.getElementsByTagName 'strong'
for strongElement in StrongElements 
  strongElement.fontWeight = 'bold'
  strongElement.color = 'red'





strongStyle:
  fontWeight: 'bold'
color: 'red'
strongElements = document.getElementsByTagName 'strong' for strongElement in StrongElements for styleName, styleValue of strongStyle strongElement[styleName] = styleValue





assert = require 'assert'
haystack = [1..900] needle = 6 assert needle in haystack





assert 'fundamental'.indexOf('fun') >= 0





expect('fundamental').to.contain 'fun'





HELO coffeescriptinaction.com                             
250 OK                                                    
MAIL FROM: scruffy@coffeescriptinaction.com               
250 OK - mail from <scruffy@coffeescriptinaction.com>     
RCPT TO: agtron@coffeescriptinaction.com                  
250 OK - Recipient <agtron@coffeescriptinaction.com>      
DATA                                                      
354 Send data.  End with CRLF.CRLF                        
Hi Agtron. Just Scruffy testing SMTP.
250 OK QUIT





class Smtp
  constructor: ->
  connect: (host, port=25) ->
  send: (message, callback) ->





smtp = new Smtp
smtp.connect 'coffeescriptinaction.com'
smtp.send 'MAIL FROM: scruffy@coffeescriptinaction.com', (response) ->
  if response.contains 'OK'
    smtp.send 'RCPT TO: agtron@coffeescriptinaction.com', (response) ->





scruffysEmail = new Email
  to: ''
  from: ''
  body: '''
'''
scruffysEmail.send()





> npm install simplesmtp





Listing 8.2 An object literal?based DSL for email (email.coffee)

simplesmtp = require 'simplesmtp'
class Email SMTP_PORT = 25 SMTP_SERVER = 'coffeescriptinaction.com'
constructor: ({@to, @from, @subject, @body}) ->
send: -> @client = simplesmtp.connect SMTP_PORT, SMTP_SERVER @client.once 'idle', -> @client.useEnvelope from: @from
to: @to
@client.on 'message', -> client.write """ From: #{@from} To: #{@to}
Subject: #{@subject}
#{@body}
""" client.end()





scruffysEmail = new Email                      
  to: 'agtron@coffeescriptinaction.com'        
  from: 'scruffy@coffeescriptinaction.com'     
  subject: 'Hi Agtron!'                        
body: '''
This is a test email.
'''
scruffysEmail.send() # { to: 'agtron@coffeescriptinaction.com', # from: 'scruffy@coffeescriptinaction.com', # subject: 'Hi Agtron!', # body: '\nThis is a test email. \n ' } # Error: connect ETIMEDOUT





scruffysEmail = new Email
scruffysEmail
.to('agtron@coffeescriptinaction.com')
.from('scruffy@coffeescriptinaction.com')
.body '''
Hi Agtron!
'''
scruffysEmail.send (response) -> console.log response





Listing 8.3 A fluent interface?based DSL for email

simplesmtp = require 'simplesmtp'
class Email SMTP_PORT = 25 SMTP_SERVER = 'coffeescriptinaction.com' constructor: (options) -> ['from', 'to', 'subject', 'body'].forEach (key) => @["_{key}"] = options?[key] @[key] = (newValue) -> @["_#{key}"] = newValue
@
send: -> client = simplesmtp.connect SMTP_PORT, SMTP_SERVER client.once 'idle', -> client.useEnvelope from: @_from to: @_to client.on 'message', -> client.write """ From: "#{@_from}" To: #{@_to}
Subject: #{@_subject}
#{@_body}
""" client.end()
@
scruffysEmail = new Email()
scruffysEmail .to('agtron@coffeescriptinaction.com') .from('scruffy@coffeescriptinaction.com') .subject('Hi Agtron!')
.body '''
This is a test email.
'''
scruffysEmail.send()





send = (next) ->
http.send next()
email = ->





send email (body 'Hi Agtron') to 'agtron@coffeescriptinaction.com'





send email \
(body 'Hi Agtron!')\
(to 'agtron@coffeescriptinaction.com')





loggedIn = -> true
doctype 5 html -> body -> ul class: 'info', -> li -> 'Logged in' if loggedIn()





<!DOCTYPE html>
<html>
  <body>
    <ul class='info'>
      <li>Logged in</li>





Listing 8.4 A basic DSL for HTML

doctype = (variant) ->        
  switch variant              
    when 5                    
"<!DOCTYPE html>"
markup = (wrapper) -> (attributes..., descendents) -> attributesMarkup = if attributes.length is 1 ' ' + ("#{name}='#{value}'" for name, value of attributes[0]).join ' ' else ''
"<#{wrapper}#{attributesMarkup}>#{descendents() || ''}</#{wrapper}>"
html = markup 'html' body = markup 'body' ul = markup 'ul' li = markup 'li'





emphasis = ->
fontWeight: 'bold'
css 'ul': emphasis() '.x': fontSize: '2em'





ul {
  font-weight: bold;
}
.x {
  font-size: 2em;
}





Listing 8.5 A basic DSL for CSS

css = (raw) ->
  hyphenate = (property) ->                                     
    dashThenUpperAsLower = (match, pre, upper) ->               
      "#{pre}-#{upper.toLowerCase()}"                           
property.replace /([a-z])([A-Z])/g, dashThenUpperAsLower
output = (for selector, rules of raw #B rules = (for ruleName, ruleValue of rules "#{hyphenate ruleName}: #{ruleValue};" ).join '\n' """ #{selector} { #{rules} } """ ).join '\n'





SELECT '*' FROM 'users' WHERE 'name LIKE "%scruffy%"'





query
  SELECT: '*'
  FROM: 'users'
  WHERE: 'name LIKE "%scruffy%"'





query
  SELECT: '*'
  FROM: 'users'
  WHERE: "name LIKE '%#{session.user.name}%'"





8.3 How the compiler works

I = (x) -> x         
I = λx.x     





I = (x) -> x





coffee = require 'coffee-script'
expression = 'I = (x) -> x'
coffee.tokens expression # [[ 'IDENTIFIER', 'I'], # [ '=', '='], # [ 'PARAM_START', '('], # [ 'IDENTIFIER', 'x'], # [ 'PARAM_END', ')'], # [ '->', '->'], # [ 'INDENT', 2], # [ 'IDENTIFIER', 'x'], # [ 'OUTDENT', 2], # [ 'TERMINATOR', '\n'] ]





I      =      (          x         )     ->            x     





I = (x) -> x





I = (x) ->[INDENT]x





a = 1
b = 2





a      =    1        \n         a     =     2        \n





I = (x) -> x
I 2





I          (         2         )         \n





f ->
  a
.g b, ->
  c
.h a





play       (    'football'   )     unless   injured      \n





coffee = require 'coffee-script'
expression = 'I = (x) -> x'
tokens = coffee.tokens expression
coffee.nodes tokens # { expressions: # [ { variable: [Object], # value: [Object], # context: undefined, # param: undefined, # subpattern: undefined } ] }





console.log JSON.stringify coffee.nodes, null, 2





{
  "expressions": [
    {
      "variable": {
        "base": {
          "value": "I"
        },
        "properties": []
      },
      "value": {
        "params": [
          {
            "name": {
              "value": "x"
            }
          }
        ],
        "body": {
          "expressions": [
            {
              "base": {
                "value": "x"
              },
              "properties": []
            }
          ]
        },
        "bound": false
      }
    }
  ]
}





8.4 Bending code to your ideas

node> eval('var x = 2');
node> x
node> # 2





coffee = require 'coffee-script'
coffee.eval '2'
# 2
evaluation = coffee.eval '2 + 4'
# 6
evaluation # 6





coffee.eval '''
x = 1
y = 2
x + y'''
# 3
x
# Reference Error: x is not defined
y # Reference Error: y is not defined





coffee = require 'coffee-script'
x = 42
y = coffee.eval "#{x} + 3"
y # 45





coffee = require 'coffee-script'
scruffyCode = ''' I = λx.x
'''
coffeeCode = scruffyCode.replace /λ([a-zA-Z]+)[.]([a-zA-Z]+)/g, '($1) -> $2' identity = coffee.eval coffeeCode identity 2
#2
hello = identity (name) -> "Hello #{name}"
# [Function]
hello 'Scruffy' # 'Hello Scruffy'





Listing 8.6 ScruffyCoffee with eval and regular expressions

fs = require 'fs'
coffee = require 'coffee-script'
evalScruffyCoffeeFile = (fileName) -> fs.readFile fileName, 'utf-8',(err, source) -> coffeeCode = source.replace /λ([a-zA-Z]+)[.]([a-zA-Z]+)/g,'($1) -> $2'
coffee.eval coffeeCode
fileName = process.argv[2] unless fileName console.log 'No file specified' process.exit() evalScruffyCoffeeFile fileName





I = λx.x





I     =     λx     .      x          \n





coffee = require 'coffee-script'
scruffyCoffee = ''' I = λx.x
'''
tokens = coffee.compile scruffyCoffee
i = 0 while token = tokens[i] # handle token i++





Listing 8.7 Custom rewriter

fs = require 'fs'
coffee = require 'coffee-script'
evalScruffyCoffeeFile = (fileName) -> fs.readFile fileName, 'utf-8', (error, scruffyCode) -> return if error
tokens = coffee.tokens scruffyCode
i = 0 while token = tokens[i] isLambda = token[0] is 'IDENTIFIER' and /^λ[a-zA-Z]+$/.test token[1] if isLambda and tokens[i + 1][0] is '.' paramStart = ['PARAM_START', '(', {}] param = ['IDENTIFIER', token[1].replace(/λ/, ''), {}] paramEnd = ['PARAM_END', ')', {}] arrow = ['->', '->', {}] indent = ['INDENT', 2, generated: true] tokens.splice i, 2, paramStart, param, paramEnd, arrow, indent j = i while tokens[j][0] isnt 'TERMINATOR' j++ outdent = ['OUTDENT', 2, generated: true] tokens.splice j, 0, outdent i = i + 3 continue i++ nodes = coffee.nodes tokens javaScript = nodes.compile()
eval javaScript
fileName = process.argv[2] process.exit 'No file specified' unless fileName evalScruffyCoffeeFile fileName





coffee = require 'coffee-script'
nodes = coffee.nodes '2 + 1'





addition = nodes.expressions[0]
addition.operator
# '+'
addition.first.base.value
# '2'
addition.second.base.value # '1'





nodes.compile bare: true
# 'return 2 + 1;'





addition.operator = '-'
nodes.compile bare: true
# 'return 2 - 1'





Listing 8.8 Generating method tests via the AST

fs = require 'fs'
coffee = require 'coffee-script'
capitalizeFirstLetter = (string) ->
string.replace /^(.)/, (character) -> character.toUpperCase()
generateTestMethod = (name) ->
"test#{capitalizeFirstLetter name}: -> assert false"
walkAst = (node) ->
generated = "assert = require 'assert'"
if node.body?.classBody className = node.variable.base.value methodTests = for expression in node.body.expressions if expression.base?.properties methodTestBodies = for objectProperties in expression.base.properties if objectProperties.value.body? generateTestMethod objectProperties.variable.base.value methodTestBodies.join '\n\n ' methodTestsAsText = methodTests.join('').replace /^\n/, '' generated += """ \n class Test#{className}
#{methodTestsAsText}
test = new Test#{className} for methodName of Test#{className}:: test[methodName]()
"""
expressions = node.expressions || [] if expressions.length isnt 0 for expression in node.expressions generated = walkAst expression
generated
generateTestStubs = (source) -> nodes = coffee.nodes source
walkAst nodes
generateTestFile = (fileName, callback) -> fs.readFile fileName, 'utf-8', (err, source) -> if err then callback 'No such file' testFileName = fileName.replace '.coffee', '_test.coffee' generatedTests = generateTestStubs source
fs.writeFile "#{testFileName}", generatedTests, callback 'Done'
fileName = process.argv[2]
unless fileName console.log 'No file specified'
process.exit()
generateTestFile fileName, (report) -> console.log report





class Elephant
  walk: ->
    'Walking now'
  forget: ->
    'I never forget'





> coffee 8.8.coffee elephant.coffee
> # Generated elephant_test.coffee





assert = require 'assert'
class TestElephant
testWalk: -> assert false
testForget: -> assert false
test = new TestElephant for methodName of TestElephant:: test[methodName]()





let x = 3
  console.log x
# 3
console.log x # ReferenceError: x is not defined





if true
  let x = 2, y = 2
  console.log 'let expression'
  console.log 'wraps block in closure'





var ok;
ok = require('assert').ok;
if (true) { (function(x, y) { console.log('let expression'); return console.log('wraps block in closure'); })(2, 2);
}
ok(typeof x === "undefined" || x === null);
ok(typeof y === "undefined" || y === null);





do (x = 2, y = 2) ->
  console.log 'let expression'
  console.log 'wraps block in closure'





Listing 8.9 Scruffy?s let implementation using a custom rewriter

fs = require 'fs'
coffee = require 'coffee-script'
evalScruffyCoffeeFile = (fileName) -> fs.readFile fileName, 'utf-8', (error, scruffyCode) -> letReplacedScruffyCode = scruffyCode.replace /\slet\s/, ' $LET ' return if error
tokens = coffee.tokens letReplacedScruffyCode
i = 0 consumingLet = false waitingForOutdent = false while token = tokens[i] if token[0] is 'IDENTIFIER' and token[1] is '$LET' consumingLet = true doToken = ['UNARY', 'do', spaced: true] tokens.splice i, 1, doToken else if consumingLet if token[0] is 'CALL_START' paramStartToken = ['PARAM_START', '(', spaced: true] tokens[i + 1][2] = 0 tokens.splice i, 1, paramStartToken if token[0] is 'CALL_END' paramEndToken = ['PARAM_END', ')', spaced: true] functionArrowToken = ['->', '->', spaced: true] indentToken = ['INDENT', 2, generated: true] tokens.splice i, 2, paramEndToken, functionArrowToken, indentToken consumingLet = false waitingForOutdent = true else if waitingForOutdent if token[0] is 'OUTDENT' or token[0] is 'TERMINATOR' outdentToken = ['OUTDENT', 2, generated: true] tokens.splice i, 0, outdentToken waitingForOutdent = false
i++
nodes = coffee.nodes tokens
javaScript = nodes.compile()
eval javaScript
fileName = process.argv[2] process.exit 'No file specified' unless fileName evalScruffyCoffeeFile fileName





8.5 Summary