0

I need to do some advanced array sorting in CoffeeScript and I came across the thenBy.js micro-library which perfectly fit my needs. It's written in JavaScript, so I translated it to CoffeeScript so that I could use it inline in my .coffee file, and I ran into some problems with the translation. This doesn't work:

firstBy = ->

  # mixin for the `thenBy` property
  extend = (f) ->
    f.thenBy = tb
    return f

  # adds a secondary compare function to the target function (`this` context)
  #which is applied in case the first one returns 0 (equal)
  #returns a new compare function, which has a `thenBy` method as well
  tb = (y) ->
    x = this
    return extend((a, b) ->
      return x(a, b) or y(a, b)
    )
  return extend

However, if I wrap with parens and put on trailing parens, it does work:

### Notice the starting paren
firstBy = (->

  # mixin for the `thenBy` property
  extend = (f) ->
    f.thenBy = tb
    return f

  # adds a secondary compare function to the target function (`this` context)
  #which is applied in case the first one returns 0 (equal)
  #returns a new compare function, which has a `thenBy` method as well
  tb = (y) ->
    x = this
    return extend((a, b) ->
      return x(a, b) or y(a, b)
    )
  return extend
)() ### <- Notice the ending parens

I'm having a no luck understanding why putting those trailing parens on the thing causes it to work. I understand that I have an anonymous function, and I'm then calling it with those parens (see this answer), but why does that work?

Community
  • 1
  • 1
mattmc3
  • 17,595
  • 7
  • 83
  • 103
  • Can you compile to JS with both versions and see the difference? – rdodev Nov 29 '13 at 01:29
  • Yes. They both compile just fine, and the parens are the only difference between the two in the output JS as well as the input. It's really just a matter of me not understanding how this clever little snippet is actually doing its work. Why do you have to call this while defining it to get it to work? – mattmc3 Nov 29 '13 at 01:32
  • Can you try using `=>` as opposed to `->` for `extend` and `tb` only in the first example and let me know if that works for you? – rdodev Nov 29 '13 at 01:39

1 Answers1

1

The trailing parens are calling the function larger function (the one defined as firstBy in your first example), effectively setting the variable to the function's return value: the extend function. In other words:

# Let firstBy1 be equivalent to your first definition, and
# firstBy2 equivalent to your second. Then
# firstBy1() is functionally equivalent to firstBy2

console.log firstBy2
### logs:
#   function (f) {
#     f.thenBy = tb
#     return f
### }

# So:
func = (x, y) ->
  # ... some comparison of x and y...

foo = firstBy2(func) # or `foo = firstBy1()(func)`
console.log foo.thenBy
### logs:
#   function (y) {
#     x = this
#     return extend(function (a, b) {
#       return x(a, b) or y(a, b)
#     })
### }

This is probably best illustrated with an example:

# For brevity/clarity...
firstBy = firstBy2

randomNums =
  { attr1: 1, attr2: 2 },
  { attr1: 2, attr2: 8 },
  { attr1: 4, attr2: 2 },
  { attr1: 5, attr2: 2 },
  { attr1: 5, attr2: 3 },
  { attr1: 6, attr2: 1 },
  { attr1: 3, attr2: 1 },
  { attr1: 2, attr2: 4 }

func1 = (x, y) ->
  if x.attr1 == y.attr1
    return 0
  if x.attr1 > y.attr1 then 1 else -1

func2 = (x, y) ->
  if x.attr2 == y.attr2
    return 0
  if x.attr2 > y.attr2 then 1 else -1

When we call...

randomNums.sort(firstBy(func1).thenBy(func2))

...firstBy(func1) is evaluated first. It returns func1 with a thenBy attribute:

func1 = (x, y) -> 
  if x.attr1 == y.attr1
    return 0
  if x.attr1 > y.attr1 then 1 else -1

func1.thenBy = (y) ->
  x = this
  return extend((a, b) ->
    return x(a, b) or y(a, b)
  )

So then, calling firstBy(func1).thenBy(func2) calls the newly added thenBy parameter attached to func1, yielding:

func1.thenBy(func2) =
  extend((a, b) ->
    func1(a, b) or func2(a, b)
  )

The extend function then applies another (uncalled) thenBy attribute to this anonymous function, yielding the final function that sort uses to order randomNums. It's basically calling func1 on each pair of numbers in the array, and in the event that func1 returns 0 (when the attr1's of each object are equal), then func2 is evaluated on the same pair. This can be extrapolated endlessly with more calls to thenBy. In the end, our function call returns:

[
  { attr1: 1, attr2: 2 },
  { attr1: 2, attr2: 4 },
  { attr1: 2, attr2: 8 },
  { attr1: 3, attr2: 1 },
  { attr1: 4, attr2: 2 },
  { attr1: 5, attr2: 2 },
  { attr1: 5, attr2: 3 },
  { attr1: 6, attr2: 1 }
]

Hope that helps!

mtoor
  • 218
  • 2
  • 7
  • Ah, so this is what enables the fluent API mechanism of this, ie `firstBy(???).thenBy(???).thenBy(???).etc`? – mattmc3 Nov 29 '13 at 02:02
  • 1
    Yeah, because both firstBy and thenBy return instances of the internal extend function. This code is frustratingly recursive, I'll an example to the post for clarity. – mtoor Nov 29 '13 at 02:14
  • It's also worth noting you can get the same functionality using the `do` keyword! Instead of using `(` at the start and `)()` at the end, you can just change it to `firstBy = do ->`... and that will call the function. You also don't need to explicitly tell CoffeeScript to `return` anything. – phenomnomnominal Nov 29 '13 at 07:12