Skip to content

Instantly share code, notes, and snippets.

@aesnyder
Last active August 29, 2015 14:01
Show Gist options
  • Save aesnyder/9923d5be47cfbc8a1e38 to your computer and use it in GitHub Desktop.
Save aesnyder/9923d5be47cfbc8a1e38 to your computer and use it in GitHub Desktop.
_.chainable
# takes an object of methods and makes them optionally chainable,
# all lodash methods are accessible in the chain too
#
# The following methods are added as well:
# attr: returns value of attribute on given object
# inlcuding: extends either object, or each object in collection
# with attrName: callback(object)
# if no callback is given then attrName is assumed to be a function
# passed to _.chainable: attrName: attrName(object)
#
# playerData = [
# { name: 'Bobby', kills: 125, deaths: 63, shots: 128 }
# { name: 'Annie', kills: 201, deaths: 14, shots: 2432 }
# { name: 'Jacob', kills: 101, deaths: 188, shots: 201 }
# ]
#
# players = _.chainable
# kdr: (p) ->
# p.kills / p.deaths
# accuracy: (p) ->
# p.kills / p.shots
# score: (p) -> @kdr(p) + @accuracy(p)
#
# players(playerData).including('score').max('score').attr('name').value() // Bobby
# players(playerData).find(name: 'Bobby').kdr().value() // 1.9841269841269842
# players(playerData).find(name: 'Bobby').including('kdr').value() // { name: 'Bobby', kills: 125, deaths: 63, shots: 128, kdr: 1.9841269841269842 }
_.mixin 'chainable': (obj) ->
methods = _.extend _.cloneDeep(obj), _,
including: (obj, attrName, value) ->
extObj = (o) ->
val = if _.isFunction(value)
value.call(methods, o)
else if _.isString(value)
value
else
methods[attrName](o)
_.extend o, _.object([attrName], [val])
if _.isArray(obj)
_.map obj, extObj, methods
else if _.isObject(obj)
extObj(obj)
attr: (obj, metric) -> obj[metric]
_.extend (arg) ->
_.extend
__collector: _.cloneDeep(arg)
value: -> @__collector
,
_.mapValues methods, (method) ->
->
args = _.toArray arguments
args.unshift(@__collector)
@__collector = method.apply(methods, args)
this
, methods
@RocketPuppy
Copy link

There's a paradigm shift here, and it's a big one. Like all big changes it will look odd at first. It's a little like learning to read a new language. There's a different grammar and style of thinking required that takes practice to develop. I have shown tangible gains though. Programming with function composition is more flexible. As for readability, there are a number of ways to improve readability. The easiest is to use a language that was designed from the ground up to support this like Haskell. You could also use something like Purescript, but really Javascript can support this fairly easily.

To increase readability in Javascript (well Coffeescript) we can hang the composition operator off of the Function prototype so our composition looks more like using the dot operator. We can do the same thing with partial application using $ on the Function prototype. The end result being something that could look like:

Function.prototype.c = (g) -> c(this, g)
Function.prototype.$ = (args...) -> _.partial(this, args...)

findCurrentUser = find.$((o) -> o.name == currentUser.name))

#alias sideEffect so it looks more familiar
tap = sideEffect

includeScore.
  c includeRank.
  c maxBy('rank').
  c tap(renderMaxScoreTemplate).
  c tap(renderUserStandingsSidebar).
  c findCurrentUser.
  c tap(renderCurrentUserDetails)

This looks almost the same as your original chain, and it's more modular. We can split it however we want, insert things wherever we want, and manipulate it any way we want. There's no wrapper that we need to go through.

That's all I really have to say. I've shown some tangible gains to using function composition over just chainable. Perhaps my last code sample is more readable than the previous ones. I don't really think so, but I'm used to reading this style of code. Let me know if you have any other questions, I'd be happy to answer them.

EDIT: Updated code to have proper line continuations.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment