Destructuring arguments in CoffeeScript


CoffeeScript has a good and fairly undocumented feature — destructuring arguments. It’s very similar to binding forms from clojure and a bit like pattern matching like in scala and erlang. It has syntax similar to destructuring assignment but works with functions (and methods) arguments.

Simple examples:

getUppercaseName = ({name}) -> name.toUpperCase()
getUppercaseName name: 'test'
# 'TEST'

head = ([x, ...]) -> x
head [1, 2, 3, 4]
# 1

tail = ([_, xs...]) -> xs
tail [1, 2, 3, 4]
# [ 2, 3, 4 ]

prettifyUser = ({groups: {names: groupNames}, username}) ->
    "#{username}: #{groupNames.join(', ')}"
prettifyUser 
    groups: {names: ['a', 'b', 'c']}
    username: 'user'
# 'user: a, b, c'

Looks useful? But will be more if we combine it with underscore.js. Assume we need to transform result of $.serializeArray() like:

[
    {"name": "title", "value": "Test Title"},
    {"name": "text", "value": ""},
    {"name": "tags", "value": "Python"},
    {"name": "tags", "value": "CoffeeScript"},
    {"name": "tags", "value": "Clojure"},
    {"name": "csrfmiddlewaretoken", "value": "token"}
]

To something like (fields with blank value and service fields should be removed, values of fields with same name should be presented as a list):

{
    "title": "Test Title",
    "tags": ["Python", "CoffeeScript", "Clojure"]
}

Let’s start with imperative solution:

transform = (serializedForm) ->
    groupByName = ->
        result = {}
        for field in serializedForm
            if field.value != '' and field.name != 'csrfmiddlewaretoken'
                if result[field.name]
                    result[field.name].push field.value
                else
                    result[field.name] = [field.value]
        result
    
    flattenFieldsWithSingleValue = ->
        result = {}
        for name, value in groupByName()
            if value.length == 1
                result[name] = value[0]
            else
                result[name] = value
        result
    
    flattenFieldsWithSingleValue()

It works, but looks ugly and complex.

Now look to implementation with underscore.js and destructuring arguments:

transform = (serializedForm) ->
    _.chain serializedForm
     .reject ({name, value}) ->
         value == '' or name == 'csrfmiddlewaretoken'
     .groupBy 'name'
     .map (vals, name) ->
         vals = _.map(vals, ({value}) -> value)
         [name, if vals.length > 1 then vals else vals[0]]
     .object()
     .value()

A lot better I think. Less lines of code, each transformation step can be simple separated from each other. And all this because of fluent interface presented by underscore.js _.chain and destructuring arguments.



comments powered by Disqus