Skip to content

Instantly share code, notes, and snippets.

@davo
Created August 29, 2017 11:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save davo/59c7d9e908fb8ab9c5014c2e246814c1 to your computer and use it in GitHub Desktop.
Save davo/59c7d9e908fb8ab9c5014c2e246814c1 to your computer and use it in GitHub Desktop.
## Domain Specific Languages in Coffee Script
# ## Title
# ## What's Domain Specific Languages
# ## Great Examples in Coffee Script:
#### Socket.io
#### ZappaJS
#### Orpheus
class User extends Orpheus
constructor: ->
@has 'book'
@str 'about_me'
@num 'points'
@set 'likes'
@zset 'ranking'
@map @str 'fb_id'
@str 'fb_secret'
user('rada')
.name.hset('radagaisus')
.points.hincrby(5)
.points_by_time.zincrby(5, new Date().getTime())
.books.sadd('dune')
# ## Tips & Tricks for writing DSLs in Coffee Script
#### Object.defineProperties
# Let's start with cool simple tricks. `Object.defineProperties`
# and `__defineGetter__` are cool properties that aren't supported
# in a cross browser fashion, but they give a cool sense of what
# we are talking about here, and if you write NodeJS you can, and
# should, use them extensively.
# So `__defineGetter__` is an obsolete function that was supported
# in FF and Chrome. It can set properties on the object prototype
# and when those properties are referenced it calls a function.
# `__defineGetter__` is responsible for the cool socket.io DSL:
socket.broadcast.emit 'joined',
user: user.to_json()
# Here broadcast is used as a flag to note that this message
# should be send to all the users who are listening. It's implemented
# like this:
Client.prototype.__defineGetter__('broadcast', function () {
this.flags.broadcast = true;
});
# So `__defineGetter__`, and the more complicated `Object.defineProperty`
# gives us a really cool way for adding flags to our libraries. We will
# abstract this later in a cool fashion.
#### Writing Dynamic Functions
# There are a few cases where you want to write several functions in
# one go. Those functions might do very similar things so you think
# to yourself, because you like to abstract things, because you are
# a programmer, 'Hey, I can abstract this!'. Now you have two problems.
# It's pretty simple to create these dynamic functions in JS and CS,
# the only pitfall is that you have to make sure you closure all your
# variables correctly.
# Remember this nasty problem?
for (var i = 0; i < 4; i++) {
setTimeout(function() {
console.log(i);
}, i*100);
}
# By the time the anonymous function inside setTimeout gets called,
# `i` is equal to 4, so all the 4 functions prints 4!
# So to fix this we do:
for (var i = 0; i < 4; i++) {
setTimeout(
(function(i) {
return function() {
console.log(i);
}
})(i)
, i*100);
}
# Which looks really ugly, but it creates a closure - another anonymous function
# that covers up i as a local variable and makes sure it's protected from the loop.
# In CoffeeScript it's way nicer:
for i in [0...4]
do (i) ->
setTimeout ->
console.log i
, i * 100
# If we'll have time we'll add later a helper that flips the setTimeout arguments
# to get this:
flip = (fn) ->
return ->
fn.apply(this, arguments.slice(0).reverse())
timeout = flip setTimeout
for i in [0...4]
do (i) ->
timeout i * 100, ->
console.log i
# Which is as clean as it gets.
# So we got that covered. Not let's actually write metaprogramming functions.
# We'll start with a simple and easy wrapper that lets us use the functions
# `error`, `warn`, `debug`, `log` and `info` even when we don't have the
# console available.
# Let's start by defining a global property that checks is we are in dev mode
# or in production:
window._debug =
live : !! document.location.host.match 'mysite.com'
# Sets the debug level to 5 on development to show
# all messages and to 0 in production to show only
# errors. Tweak these values in the console if you
# want to see more / less errors.
if debug.live
then debug.level = 0
else debug.level = 5
# An easy way to see all the logs and filter them
window.logs = []
# Adds an `error()`, `warn`, `info()` and `log()` functions
# to the window object. These functions are similar to the
# console functions available in sane browsers. They:
#
# - Delegate displaying the messages to the console,
# if the console is available and the log level is
# high enough.
# - Pushes the log messages as {msg: 'string', data: {...}}
# to window.logs for better inspection and filtering.
#
for msg_type, log_level in ['error', 'warn', 'info', 'log']
do (msg_type, log_level) =>
@[msg_type] = (msg = '', data) ->
# Log to Console
if _debug.live and _debug.level >= log_level
if not $.browser.msie and console?
console[msg_type].apply console, Array::slice.call arguments
# Add to our logs
window.logs.push
msg : msg
data: data
# Awesome. Now, a way more complicated example. We showed Orpheus before.
# What it does is give you a syntax similar to ActiveRecord for your redis
# objects:
class User extends Orpheus
constructor: ->
@has 'book'
@str 'about_me'
@num 'points'
@set 'likes'
@zset 'ranking'
@map @str 'fb_id'
@str 'fb_secret'
# And it gives you a nice way to query it with chaining:
user('Almog')
.points.hincrby(5)
.ranking.zincrby(5, new Date().getTime())
.books.sadd('dune')
# How it does this, basically, is that is has a large list of all the redis
# commands:
str: [
'hdel',
'hexists',
'hget',
'hsetnx',
'hset'
]
# And what it does is it goes through all the model properties,
# and based on each property type it creates the relevant functions
# on the model. So, a way simpler implementation of this can look like:
for property, info of @model
# Creates the property on the object
@[key] = {}
# Creates the property commands
for f in commands[info.type]
@[key][f] = (args...) =>
@_commands.push args
# BackboneJS and most other MVCs do the same thing with the events object
class View extends Module
@include Events
events: {}
# The constructor builds the access to the parameters received,
# passes them to initialize and delegates the events
constructor: (o) ->
# o - el, template, everything you need to access with 'this'
_.extend this, o
@initialize(o)
@delegate_events()
_delegateEventSplitter: /^(\S+)\s*(.*)$/
# Add event listeners from the @events hash
delegate_events: ->
# Match the event and the element, separated by space
events = for k,v of @events
k.match(/^(\S+)\s*(.*)$/)[1..2].concat v
for [event,element,fn] in events
do (event, element, fn) =>
_fn = (e) =>
@[fn](e, @$(e.currentTarget))
# $('.view').on 'click', '.view-header', func
if element
then @$(@el).on event, element, _fn
else @$(@el).on event, _fn
#### Using the Context
# So context switching is kind of a more complicated issue, but bear with
# me here. Context switching, or 'Using the Context', is a simple method
# that gives functions a different context to operate on. Context means all
# all local `this` variables the function has access to. A good example of
# this ZappaJS:
require('zappajs') ->
@get '/': ->
@render 'index',
title: 'Zappa!'
stylesheet: '/index.css'
@get '/logout', ->
@request.logOut()
@response.redirect 'back'
@post '/:name/data.json': ->
@send
email: "#{@params.name}@example.com"
# So the cool thing that Zappa does here is that you get all
# this special variables like `@params` and `@render` available
# to you through the local this context. What's even more cool
# is that code highlighting for `@functions` is differnet and
# that's nice.
# You don't need to receive this variables as arguments or
# anything, they are just available in the context. The
# trick to accomplish this is very simple.
hello = (fn) ->
fn.apply {name: 'Almog'}, ['Almog']
hello = ->
console.log "Hello #{@name}"
# So `.apply` is a really cool tool. It's a function that's
# sits on `Function` prototype. It calls a that `Function`
# with the first argument to `.apply` as the context and the
# second argument as the arguments. So we could also do:
hello: = (name) ->
console.log "Hello #{name}"
# So what does zappa do is create a giant list of context
# variables:
ctx =
app : app
settings: app.settings
request : req
query : req.query
params : req.params
body : req.body
session : req.session
response: res
next : next
send : -> res.send.apply res, arguments
json : -> res.json.apply res, arguments
# And then call your function with them:
fn.apply(ctx, [])
#### Referencing Object Literals with the Fat Arrow
# This is a hack, and it should be used with caution. This hack
# allows us to write object literals and reference them with @
# or this, without problems:
# A very ugky hack to access object literals with the fat arrow.
# So the important line is `_this = obj` where obj is the name
# of the object. And then you work on the object regularly by
# extending it and adding functions to it. This means we can
# reference the object literal with the this shorthand, and we
# can also *bind* functions to it, which is great.
_this = obj = {}
$.extend obj,
hello: => @stuff()
# And it compiles to:
var obj,
_this = this;
_this = obj;
$.extend(obj, {
hello: function() {
return _this.stuff()
}
});
#### Mixins and Modules
#### Publish / Subscribe
#### Hooks
#### Building Fixtures
#### Working with Arguments
# A lot of libraries offer functions that can receive a variadic number
# of arguments and behave differently based on the number of arguments,
# the type of the arguments and even the values of the arguments. Just
# think about jQuery's `$.Ajax()` or `$.fn.animate()` that have tons of
# different options and function signatures. You can call animate like
# this:
# `$.fn..animate( properties [, duration] [, easing] [, complete] )`
$('#text-box').animate left: '500px', 1000, 'swing', ->
alert "Animation Completed!"
# Or like this:
# `$.fn..animate( properties, options )`
$('#text-box').animate
left: '500px'
top : '100px'
,
easing : 'swing'
duration: 1000
complete: -> alert "Animation Completed!"
# So there are a lot of ways we want to call functions.
# Sometimes we need to support ugly versions to be backward
# compatible. Sometimes we have functions that tries to do
# convention over configuration and gives a lot of default
# values.
# Let's go over a few things that help us deal with this when
# we write our own libraries.
# In JavaScript, there are a lot of weird special variables
# for dealing with functions: `arguments` probably is the most
# well known.
# `arguments` is a special local property of `Function` that
# is kinda like an array (it has a `.length` propery) but
# kinda not like an array (you can't slice and dice it).
# You got a lot of sometimes obsolete or deprecated or not
# cross-browser supported properties of `arguments`:
# Say we call this function with `fn(1,2,3)`
fn = ->
# [1,2,3]
log arguments
# this function, as a string
log arguments.callee
# The function that called this function
log arguments.callee.caller
# The Arguments length: 3
log arguments.length
# Proper way of converting arguments to an array:
Array::slice.call arguments
# In CoffeeScript we got a few additions to our arsenal.
# First, we can use default values:
say = (text = "Hello, World") -> text
say() # 'Hello, World'
say "Hi" # 'Hi'
# Note that Coffee uses `text == null` to set the default:
say = function(text) {
if (text == null) {
text = "Hello, World";
}
return console.log(text);
};
# By the way, you can add a lot of funky stuff as the default value:
Requests.Monitor = (@request = $.ajax('/user.json')) ->
@request.done (data) ->
log "The Server Responded with", data
# This, ugly and highly discouraged code, compiles to:
Requests.Monitor = function(request) {
this.request = request != null ? request : $.ajax('/user.json');
return this.request.done(function(data) {
return log("The Server Responded with", data);
});
};
# Okay. So another cool thing with Coffee is Splats. Splats group together
# a number of arguments as an array.
# Sums a variadic list of numbers. `.reduce()` only available
# in ES5 so don't use it in the browser without shims!
sum = (numbers...) -> numbers.reduce (s, n) -> s + n
# So we can call sum with any number of arguments and it will work:
sum 1, 2, 3, 4 # 10
sum 1, 2 # 3
# Here's another way more complicated example:
# AsyncJS is a great library, kinda like UnderscoreJS
# but for hadnling asynchronous control flow. This
# comes really handy for coordinating a lot of requests
# to the server, animating stuff, and server side things.
#
# `series` is a function that can take a variadic number
# of functions as arguments. Each of those functions is
# expected to call the first argument they receive - the
# callback - when they are done executing.
# So, for example, to issue a series of animation functions:
move_left = (cb) ->
$('.stuff').animate left: 500, 1000, cb
move_down = (cb) ->
$('.stuff').animate top: 500, 2000, cb
annoy = -> alert "Done!"
series move_left, move_down, annoy
# Now, for the implementation of series:
series: (tasks...) ->
completed = 0
iterate = ->
tasks[completed] ->
completed++
if completed isnt tasks.length
iterate()
iterate()
# Coffee Script does a lot of things to create correct splats. Let's
# break it down:
log = (msg='ping', args...) ->
if console and console.log
if args
then console.log(msg, args)
else console.log(msg)
# So log is a very simple way of logging stuff even in dumb browsers
# that don't have the `console` object available. Let's see the JS:
# Coffee puts all the variable declarations at the top of the file.
var log,
# it references the Array slice for breaking the arguments down
__slice = [].slice;
log = function() {
var args, msg;
# msg is the first argument
msg = arguments[0],
# If the arguments.length is bigger than 2 then args
# is all the arguments after the first one, otherwise
# it's just an empty array.
args = 2 <= arguments.length ? __slice.call(arguments, 1) : [];
# Sets the default message value
if (msg == null) {
msg = 'ping';
}
# Actual `log` logic!
if (console && console.log) {
if (args) {
return console.log(msg, args);
} else {
return console.log(msg);
}
}
};
# Another cool but more advanced thing Coffee does is the opposite
# of Splatting, breaking an array down to an arguments list:
log = (msg='ping', args...) ->
if console and console.log
console.log msg, args...
# So `console.log msg, args...` compiles to:
return console.log.apply(console, [msg].concat(__slice.call(args)));
# We'll cover `apply` and all the funky stuff that's going on here later.
# Another cool and unknown gem in JS is that there's a length property to
# functions. So, inside functions, `arguments.length` gives you the actual
# number of arguments this function received. But _outside_ functions you
# can get the number of arguments this function _expects_ with `fn.length`.
# Which is really cool. Here's a real world example for that detects
# async behavior based on `fn.length`.
# There's a nice idiom in a lot of JavaScript test frameworks
# like qUnit, Jasmine and Mocha. If your test runs asynchronously -
# uses timeouts, ajax, animations - you do something like:
describe 'Animation Callback', ->
it 'gets called', (done) ->
$('.hello').animate top: 1000, 2000, ->
done()
# So how does this frameworks know when test functions are async or sync?
# Here's a snippet from Mocha's source code:
function Runnable(title, fn) {
this.title = title;
this.fn = fn;
this.async = fn && fn.length;
this.sync = ! this.async;
this._timeout = 2000;
this._slow = 75;
this.timedOut = false;
}
# Here `fn` is the test function. If `fn` expects an argument
# (done, in our example), then we note it's an async test.
# Pretty simple, right? Ruby's Capybara breaks a lot of sweat
# to get this behavior.
# #### Few Rules of Thumb for writing DSL functions:
# - Only use default values in simple functions, as the last arguments
coffee.make = (table, suger = 1, type = "hafooch") ->
coffee.make 10
coffee.make 10, 2, 'latte'
# This is simple and logical. We always have to add the table mumber
# of the people who ordered the coffee. In larger degree suger is usually
# 1 and type is usually Hafooch.
# - Use Options Object for more complicated functions
# Objects give us a much more readable and versatile version of
# calling functions. They are also more verbose, as they are
# basically like named arguments.
# There's an idiom for adding defaults to objects. Just like when you
# write jQuery plugins, you can use `_.extend` or `$._extend` or your
# own extend to add them.
coffee.make = (table, o) ->
defaults =
sugar: 1
type : 'hafooch'
defaults = _.extend defaults, o
coffee.make = (table, o) ->
defaults =
sugar: 1
type : 'hafooch'
defaults = $.extend defaults, o
coffee.make = (table, o) ->
defaults =
sugar: 1
type : 'hafooch'
defaults = defaults[k] = v for k,v of o
# We can write a nice helper for adding the defaults idiom:
defaults = (o, defaults) ->
@options = defaults[k] = v for k,v of o
# And now you can make coffee with:
coffee.make = (table, o) ->
# Create defaults
defaults o,
sugar: 1
type : 'hafooch'
# Use them
if @options.sugar > 1
@suger(@options.sugar)
# - Use functions like blocks in Ruby, add the end
# Don't repeat setTimeout's mistage, always request
# functions as the last argument for you function.
coffee.make = (table, o, cb) ->
# Create defaults
defaults o,
sugar: 1
type : 'hafooch'
# Internal make coffee function
@create(@options)
# Serve the coffee
@serve(table)
# Call the passed callback on complete
fn(table,o,fn)
# Which is nice:
coffee.make 10, sugar: 2, ->
log "Coffee Served"
# Use Objects for Multiple Functions
# If you got a few hooks your function can use add them inside
# the options object, otherwise it's too confusing:
coffee.make
table : 5
sugar : 1
type : 'mocha'
before_serve : ->
log "Coffee about to be Served"
complete : ->
log "Coffee Served"
# ## Return Values
#### TODO: Implicit Return Values
#### Return Valuable things or nothing
#### Chaining
# Chaining is an awesome and simple pattern. You just add `return this`
# at the end of your library's functions so they can be called one
# after another. In the words of Martin Fowler:
# > Make modifier methods return the host object, so that multiple
# > modifiers can be invoked in a single expression:
# So let's create a simple chained function using `__defineGetter__`.
# The function, `$.fn.tap()` is a jQuery plugin that simply logs
# the current elements the operation is working on:
$('#hello').tap.show()
# So we can just drop in `.tap` into our code and continue as usual
# whenever we want to debug and make sure we caught the correct
# set of elements:
jQuery.fn.__defineGetter__ 'tap', ->
console.log "Working with Elements: ", this
return this
# Woho!
# Another example, from OrpheusJS:
user('rada')
.name.hset('radagaisus')
.points.hincrby(5)
.points_by_time.zincrby(5, new Date().getTime())
.books.sadd('dune')
## Writing DSLs
# At the start of this lecture we talked a bit about adding flags
# to our libraries using `__defineGetter__`. defineGetter is an
# ugly function, and most of the techniques we talked about, while
# I tried to explain them in a simple fashion, gives us a really
# complicated code. So while our library has a very cool and slick
# interface it will look ugly on the inside. An easy way to combat
# that is to write it in an even more abstract fashion using a DSL
# for writing DSLs.
# Let's write a simple DSL for adding flags to a function, just like
# the one you can find in socket.io, using everything we talked about
# in this lecture, including applying the context, and splats.
# We are going to create the function `flags` that takes a variadic
# number of arguments as flags, and a function, and returns a modifed
# version of this function that has access to the flags information.
# A good way to use the `flags` helper is to mix it in to your library.
# So we got two flags in the socket io library, one of them deprecated:
/**
* Broadcast flag.
*
* @api public
*/
Client.prototype.__defineGetter__('broadcast', function () {
this.flags.broadcast = true;
});
/**
* JSON flag (deprecated)
*
* @api public
*/
Client.prototype.__defineGetter__('json', function () {
this.flags.broadcast = true;
});
# And using the `flags` helper, TJ could have write it down like this:
class Client
constructor: ->
@mixin flags 'json', 'brodcast', @emit
emit: (data, args...) ->
if @flags.broadcast
# Logic..
if @flags.json
# Logic..
return this
# People can just use this as:
socket.broadcast.emit 'stuff'
socket.json.emit 'more stuff'
# And it will just work.
# So `flags` does a few things:
# 1. Actually create the flags on the object's prototype
# 2. Sets them to false on initialize
# 3. Resets them to false after the function has been called
flags = (flags..., fn) ->
@flags ||= {}
# Create the flags
for flag in flags
do (flag) =>
@::__defineGetter__ flag, ->
@flags[flag] = true
# Set all the flags to false
for flag in flags
@flags[flag] = false
# Reference to the old function
old_fn = fn
# Create the new function. It first
# calls the old function, passing the
# arguments, and then resets the flags
# to false.
@[fn] = ->
res = old_fn.apply(this, arguments)
for flag in flags
@flags[flag] = false
# Return value of the original function
return res
##### Lazy Loading
# Another cool trick, that I stole from GruntJS, is that you can use
# defineProperties to lazy load libraries hooked into the object.
# So when we use
utils.mkdirp
# It will lazily require it.
Object.defineProperties(utils,
[
// Wrapper to `require('mkdirp')`
'mkdirp',
// Wrapper to `require('rimraf')`
'rimraf',
// Wrapper to `require('./fetch')`, internal tarball helper using
// mikeal/request, zlib and isaacs/tar.
'./fetch'
].reduce(function( descriptor, api ) {
return (
// Add the Identifier and define a get accessor that returns
// a fresh or cached copy of the API; remove any
// non-alphanumeric characters (in the case of "./fetch")
descriptor[ api.replace(/\W/g, '') ] = {
get: function() { return require(api); }
},
// Return the |descriptor| object at the end of the expression,
// continuing the the reduction.
descriptor
);
// Prime the "initialVal" with an empty object
}, {})
);
# So let's write this in CoffeeScript as a mixin to your classes / objects:
# Helper for flipping arguments on a function
Object::flip = (fn, args...) ->
@[fn].apply(this, args.reverse())
# so we can call setTimeout with
setTimeout 1000, ->
log "timeout!"
# Mixin to lazy load packages:
packages = (packages....) ->
@defineProperties this,
packages.flip 'reduce', {}, (descriptor, api) ->
descriptor[api.replace(/\W/g, '')] = -> require api
return descriptor
#### MVC
##### Funky Mixins for Pagination, Modals, Tooltips, Form Builders, ListViews
#### Functional Programming
##### Currying
##### Applying
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment