Skip to content

Instantly share code, notes, and snippets.

@aesnyder
Last active August 29, 2015 14:01
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 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

You can use the compose method in underscorejs along with the partial method to accomplish the same thing. Also, some of these use cases can be done using reduce.

# Because underscore defines functions with their arguments in the wrong order
flip = (f) -> _.partial((l, r) -> f(r, l))
flippedFind = flip(_.find)

# players(playerData).including('score').max('score').attr('name').value() // Bobby
playerData.reduce( (memo, player) -> 
  if score(memo) >= score(player)
    memo
  else
    player
)['name']

# players(playerData).find(name: 'Bobby').kdr().value() // 1.9841269841269842
_.compose(kdr, flippedFind(name: 'Bobby')))(playerData)

# players(playerData).find(name: 'Bobby').including('kdr').value() // { name: 'Bobby', kills: 125, deaths: 63, shots: 128, kdr: 1.9841269841269842 }
_.compose((obj) -> obj[kdr] = kdr(obj), flippedFind(name: 'Bobby')))(playerData)

@aesnyder
Copy link
Author

Yeah originally we were composing functions like this but I personally think that the chaining is easier to read.#

players(playerData)
  .find(name: 'Bobby')
  .including('kdr')
  .value()

_.compose((obj) -> obj[kdr] = kdr(obj), flippedFind(name: 'Bobby')))(playerData)

Especially if you have more complex chains like we do. For example in our app it would be common to see something like this:

players(playerData)
  .including('score')
  .including('rank')
  .max('rank')
  .tap(renderMaxScoreTemplate)
  .tap(renderUserStandingsSidebar)
  .find(name: currentUser.name)
  .tap(renderCurrentUserDetails)
  .value()

What would that look like with pure composition?

@markalfred
Copy link

Does using _.compose offer any benefits though? One of the benefits of chaining in this way is readability, but it also offers a more modular (almost drag-and-drop) way to interact with data.

@aesnyder
Copy link
Author

Oh yeah, good point mark:

Say you wan't to get the ranks of women compared to all players:

players(playerData)
  .including('rank')
  .where(sex: 'female')
  .value()

Then your project manager is like - shit I meant to not include men in the ranking. Easy!

players(playerData)
  .where(sex: 'female')
  .including('rank')
  .value()

@RocketPuppy
Copy link

You're absolutely right about the readability. I feel part of that has to do with the fact that the underscore methods were designed to be used with underscore methods and chainable, so that their function signatures are a little awkward to work with (hence the need for flip).

On the other hand, I like to use this version of function composition:

c = (left, right) ->
  (args...) ->
    left(right(args...))

What you should look at though, is that including is basically a map, find is a kind of reduce, max is a reduce, and tap, well tap only exists to perform side effects, which is a big no-no in my book. Also remember that if you do:

new_collection = map(f, collection)
map(g, new_collection

That's the same as:

map(collection compose(g, f))

Here's some definitions:

max = (fn, collection) ->
  collection.reduce(((memo, next) ->
    memo > fn(next) ? memo : fn(next)
  ), fn(collection[0]))

maxBy = (key) ->
  _.partial(max((o) -> o[key]))

including = (key, method, collection) ->
  collection.map (o) ->
    o[key] = method(o)
    o

constant = (value) -> () -> value

includeScore = _.partial(including('score', score))
includeRank = _.partial(including('rank', rank))

# less efficient, doesn't short circuit like underscore's does but I'm illustrating a point.
find = (predicate, collection) ->
  collection.reduce((memo, next) ->
    if _.isNull(memo) && predicate(next)
      next
    else
      memo
  ), null)

findCurrentUser = _.partial(find, (o) -> o.name == currentUser.name))

split = (f, g, o) ->
 [f(o), g(o)]

sideEffect = (f) ->
  (o) ->
    f o
    o

Then, ignoring tap because it's bad.

# composition is associative, so by the magic of coffeescript we can leave off the parens!
(c (find (o) -> o.name == currentUser.name), c (maxBy 'rank'), c (including 'rank' rank), (including 'score' score))(playerData)

But of course the tap operations are important, so we need those.

maxUserByRank = c (find (o) -> o.name == currentUser.name) (maxBy 'rank')
split (c (sideEffect renderMaxScoreTemplate), c (sideEffect renderUserStandingsSidebar), maxUserByRank), (c (sideEffect renderCurrentUserDetails), findCurrentUser)

This has gotten long enough I think.

EDIT: I forgot commas. Forgive me if I forgot anymore.

@aesnyder
Copy link
Author

It certainly got longer and reduced readability, forgive me but I don't see any advantages to what you've shown. What am I missing?

@markalfred
Copy link

I see that all the same things are possible, but does compose offer some benefit over individual, chained functions, or is it personal preference?

@RocketPuppy
Copy link

How do you combine two chains together?

For example:

players(playerData)
  .including('score')
  .including('rank')
  .max('rank')
  .tap(renderMaxScoreTemplate)
  .tap(renderUserStandingsSidebar)
  .value()

players(playerData)
  .find(name: currentUser.name)
  .tap(renderCurrentUserDetails)
  .value()

I want to do both of those at separate points, but occasionally I also want to do them at the same time. If everything was a function and I was using normal, generic function composition it's easy. Using the definitions above:

doScoreAndSidebar = c (sideEffect renderMaxScoreTemplate), c (sideEffect renderUserStandingsSidebar), c (maxBy 'rank'), c (including 'rank' rank), (including 'score' score)
doUserDetails = c (sideEffect renderCurrentUserDetails), findCurrentUser

doScoreAndSidebarAndUserDetails = c doUserDetails, doScoreAndSidebar

doScoreAndSidebarAndUserDetails(playerData)

Bron said that you can do this using chainable, but I'm not sure how. I'm interesting in seeing how that would work.

The point that I'm trying to make is not that chainable is bad, it's that chainable is unnecessary. When everything is a function, composing them is simple. We don't need chainable.

To address readability, when everything is a function and you can easily compose them defining new functions is cheap. We can use that to really DRY up code and keep some of the more dense bits by themselves.

To address the objection against the length of my code: I redefined some functions because their implementation is simple, and more importantly, easily done in terms of map or reduce. Additionally it was to show just how easy and modular function composition is to use. Keep in mind you can also use _.compose instead of c. I prefer c because it acts almost like an operator when in coffeescript, but you might prefer _.compose instead. They do the same thing.

The process of wrapping up a value and then defining a composition operator for it is similar to creating a Category, which is a set of things that can be composed. These things don't have to be functions, but in this case they are.

@aesnyder
Copy link
Author

players(playerData)
  .including('score')
  .including('rank')
  .max('rank')
  .tap(renderMaxScoreTemplate)
  .tap(renderUserStandingsSidebar)
  .find(name: currentUser.name)
  .tap(renderCurrentUserDetails)
  .value()

@markalfred
Copy link

I'm not sure I understand the question

How do you combine two chains together?

The code here is the combined version of your separated one. To combine two chains, you just take the pieces of one chain and add them to the other, as easily as dragging and dropping.

players(playerData)
  .including('score')
  .including('rank')
  .max('rank')
  .tap(renderMaxScoreTemplate)
  .tap(renderUserStandingsSidebar)
  .find(name: currentUser.name)
  .tap(renderCurrentUserDetails)
  .value()

To follow your example,

doScoreAndSidebar = (data) ->
  players(data)
    .including('score')
    .including('rank')
    .max('rank')
    .tap(renderMaxScoreTemplate)
    .tap(renderUserStandingsSidebar)

Since this would return an instance of the chain, I'd just call doScoreAndData(var).value() if I needed it. To combine the two you'd do

doScoreAndSidebar(playerData)
  .find(name: currentUser.name)
  .tap(renderCurrentUserDetails)
  .value()

But this doesn't realistically offer anything over the first way, as far as I can tell.

The thing that chaining offers is very easily edited, very modular, and very readable transformations. Composing functions doesn't appear to offer any of these benefits.

@RocketPuppy
Copy link

Perhaps I should explain why all of this is desirable. I did not split the
initial code sample along some arbitrary line. It seemed to me that, looking at
this code from the outside, the original chain was doing two distinct things:
finding the user with the maximum rank (I'm going to call this findMax), and
finding the current user (I'll call this findCurrent). We then render some
things using tap, but the tapoperations are incidental as we can't perform
them without the data we initially acquire using findMax and findCurrent.

So what are the problems I see with this approach? The first is that it is not
as modular as one would first believe. I say this for the following reasons:

  1. We can only use methods that are on our initial chainable object or that
    are existing underscore methods.
  2. We have no obvious way of combining two chains together.

Why is 1) an issue? When our requirements change and we need another method
involved in the chain then we cannot simply define the method and use it as we
can with function composition. We must then also add it to our chainable
object. It's an extra hoop to jump through that can trip up developers new to
the system.

Why is 2) an issue? It hurts modularity. We can currently define one chain as a
sort of prefix chain and use it in other chains as a prefix. We cannot, however,
insert a chain into the middle of a chain, or even at the end of another chain.
Our options are limited precisely because we must call methods on the
chainable object to create chains. This modularity is desireable because it
helps us separate logical components. In this example findMax, findCurrent
and I'd also consider the tap functions as logical components because they
perform side effects.

Now, with those points enumerated, lets look and see if we can solve any of them
within the chainable framework. I don't see any way of solving point 1), so
I'm going to start with point 2). Continuing with the example of returning a
chainable object, we can define doScoreAndSidebar and doUserDetails like
so:

doScoreAndSidebar = (data) ->
  players(data)
    .including('score')
    .including('rank')
    .max('rank')
    .tap(renderMaxScoreTemplate)
    .tap(renderUserStandingsSidebar)

doUserDetails = (data) ->
  players(data) ->
    .find(name: currentUser.name)
    .tap(renderCurrentUserDetails)
    .value()

Now how will we combine them? No immediately obvious way to do this presents
itself to me, but if we reformulate doUserDetails to take a chainable
object, I think we can get something usable.

doUserDetails = (chainable) ->
  chainable
    .find(name: currentUser.name)
    .tap(renderCurrentUserDetails)
    .value()

Now I think we can do the following:

players = _.chainable _.extend players, 'doUserDetails': doUserDetails

doScoreAndSidebar(playerData).doUserDetails().value()

This feels like a kludge to me. There was a fairly laborious process we needed
to go through in order to make a composition of chainable functions that we
could use in other chains. We also need to ensure that the object we are
extending has all of the functions that we plan on using. If it doesn't we could
be in for a nasty runtime bug.

We could do it a different way. Keeping doUserDetails the same, we could
convert doScoreAndSidebar to the same format:

doScoreAndSidebar = (chainable) ->
  chainable
    .including('score')
    .including('rank')
    .max('rank')
    .tap(renderMaxScoreTemplate)
    .tap(renderUserStandingsSidebar)

This would allow us to compose them together fairly simply:

(doUserDetails doScoreAndSidebar players playerData).value()

Now we can insert and change the order of each of these "chain links" easily.
Note we still have the requirement that all of the functions in each link must
be available in the initial chainable object. This can be prohibitive when we
are trying to generate new links as we'll need to keep track of what is in each
chainable that we are using. The end result probably being that there is one
giant chainable with all of the methods we'd need. Note that if you look
closely at our final result it looks remarkably like function composition! Why
don't we just throw out the chainable abstraction then and use function
composition the whole way through? Since we're composing functions, we can do it
on any level. Starting from the functions in players we can progressively
compose larger and larger functions that are maintainable and modular.

I'd like to reformulate my previous composition examples so they might be more
palatable to read, also using the functions I defined in previous comments:

#redefine composition so it's left to right
c = (left, right) ->
  (args...) ->
    right(left(args...))

findMax = c (includeScore),
  c (includeRank),
    (maxBy 'rank')

findMax(playerData)

sE = sideEffect

withSideEffects =
  split (c findMax,
         c (sE renderUserStandingsSidebar),
           (sE renderMaxScoreTemplate)),
        (c findCurrentUser,
           (sE renderCurrentUserDetails))

withSideEffects(playerData)

Note you can trivially switch the order of composition so it reads top-down
instead of bottom-up. I personally find bottom-up nicer since it keeps the
initial arguments close to the initial function.

@aesnyder
Copy link
Author

I guess its a matter of to each his own.

I personally find your composition examples very difficult to read, and I'd say I'm more comfortable with composition than the average developer. For that reason I would have a really hard time allowing that code to make it into my projects.

Maybe for you composition will lead to more flexibility, faster development and less bugs. But I can tell you one thing for sure it will make on-boarding developers to the code base significantly harder. As an agency that ships code for others to maintain we have to be cognizant of that. For that matter we have to have a code base where you can leave the project and another developer can easily jump on and get moving. Your examples would be significantly harder for most developers to jump in and work with. Believe me I've been in projects where one clever developer wrote insanely abstracted and well written code. Something that academics would all drool at. In fact we were all drooling at the code as he was getting the PRs merged. Then when there was a bug found in the code he was the only developer out of 5 who could work on the feature. I'm telling you without a doubt that the code you posted above falls into the same category.

@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