Skip to content

Latest commit

 

History

History
229 lines (158 loc) · 5.31 KB

1.coffee.md

File metadata and controls

229 lines (158 loc) · 5.31 KB

Introducing Functional JavaScript

_ = require 'underscore'
{ok, throws, deepEqual} = require 'assert'
eq = deepEqual

Using apply, I can create a neat little function named splat that just takes a function and returns another function that takes an array and calls the original with apply, so that its elements serve as its arguments.

splat = (f) ->
  (args) -> f.apply(null, args)

sum = splat (x, y) -> x + y
ok sum([1, 2]) is 3

My variant:

splat = (f) ->
  (args) -> f args...

sum = splat (x, y, z) -> x + y + z
ok sum([1, 2, 3]) is 6

Getting off track:

Array::sum = -> @reduce ((a, b) -> a + b), 0

ok [1, 2, 3].sum() is 6

We can create a function unsplat that works opposite from splat, taking a function and returning another function that takes any number of arguments and calls the original with an array of the values given:

unsplat = (f) ->
  -> 
    args = _.toArray(arguments)   # Array.prototype.slice.call(arguments)
    f.call(null, args)

join = unsplat (array) -> array.join ' '

ok join(1, 2) is '1 2'
ok join('a', 'b', 'c') is 'a b c'

My variant:

unsplat = (f) ->
  (args...) -> f args

join = unsplat (array) -> array.join ' '

ok join(1, 2) is '1 2'
ok join('a', 'b', 'c') is 'a b c'

fail = (msg) -> throw new Error msg
warn = (msg) -> console.log "warning: #{msg}"
note = (msg) -> console.log "note: #{msg}"

isIndexed = (x) -> _.isArray(x) or _.isString(x)

The function isIndexed is also a function providing an abstraction over checking if a piece of data is a string or an array. Building abstraction on abstraction leads to the following complete implementation of nth:

nth = (arr, i) ->
  fail('index should be an integer') if not _.isNumber(i)
  fail('not supported on non-indexed types') if not isIndexed(arr)
  fail('index value out of bounds') if i not in [0..arr.length]
  arr[i]

ok nth('abc', 0) is 'a'

nums = [1..3]
throws (-> nth(nums, 6)), /out of bounds/
throws (-> nth(1000, 1)), /not supported/
throws (-> nth('abc', 'i')), /index should be an int/

second = (arr) -> nth(arr, 1)
ok second('ab') is 'b'
throws (-> second(1000)), /not supported/

numerically = (x, y) ->
  status =  0  #  x = y
  status = -1  if x < y
  status =  1  if x > y
  status

numeric = (x, y) -> x >= y    # or even `x - y`

unsorted = [2, 3, -1, -6, 0, -108, 42, 10]
expected = [-108, -6, -1, 0, 2, 3, 10, 42]

sorted = unsorted.sort numerically
eq sorted, expected

sorted = unsorted.sort numeric
eq sorted, expected

comparator = (pred) ->
  (x, y) ->
    status =  0  #  x = y
    status = -1  if pred(x, y)
    status =  1  if not pred(x, y)
    status

sorted = unsorted.sort comparator((x, y) -> x <= y)
eq sorted, expected

table = '''
  NAME  AGE  HAIR
  Bob   64   brown
  Ann   35   red
'''

csv = table.split(/\ +/).join(',')

Our csv input:

NAME,AGE,HAIR
Bob,64,brown
Ann,35,red

A small function to parse this very constrained CSV representation stored in a string is implemented as follows:

parse = (csv) ->
  rows = csv.split '\n'
  pushRowSplits = (memo, next) -> 
    memo.push(next.split(','))
    memo
  rows.reduce(pushRowSplits, [])

expected = [ 
  [ 'NAME', 'AGE', 'HAIR'  ]  # header
  [ 'Bob',  '64',  'brown' ]
  [ 'Ann',  '35',  'red'   ]
]

eq parse(csv), expected

My variant:

parse = (csv) ->
  row.split(',') for row in csv.split '\n'

table = parse(csv)

eq table, expected

data = _.rest(table).sort()

expected = [ 
  [ 'Ann',  '35',  'red'   ]
  [ 'Bob',  '64',  'brown' ] 
]

eq data, expected

Since I know the form of the original data, I can create appropriately named selector functions to access the data in a more descriptive way:

names = (data) ->
  data.map _.first

ages = (data) ->
  data.map second

hair = (data) ->
  data.map (row) -> nth(row, 2)

eq names(data), ['Ann', 'Bob']
eq ages(data),  ['35',  '64']
eq hair(data),  ['red', 'brown']

merged = _.zip names(data), ages(data)

expected = [ 
  [ 'Ann',  '35' ]
  [ 'Bob',  '64' ]
]

eq merged, expected

existy = (x) -> x?
truthy = (x) -> x? and x != false

ok existy(x) for x in [1, 'q', false, 0, '']
ok truthy(x) for x in [1, 'q', true, 0, '']

ok not existy(x) for x in [null, undefined]
ok not truthy(x) for x in [null, undefined, false]

result = [null, undefined, 1, 2, false].map(existy)
expect = [false, false, true, true, true]
eq result, expect

result = [null, undefined, 1, 2, false].map(truthy)
expect = [false, false, true, true, false]
eq result, expect

It’s sometimes useful to perform some action only if a condition is true and return something like undefined or null otherwise. Using truthy, I can encapsulate this logic in the following way:

callIf = (cond, f) -> f() if cond

_exec = (target, name) ->
  cond = target?[name]
  callIf cond, -> _.result(target, name)

eq _exec([1..3], 'reverse'), [3, 2, 1]
eq _exec(foo: 7, 'foo'), 7
eq _exec(foo: 7, 'bar'), undefined