0

I really like rambda (vs ramda), however I faced the function sortWith is missing and that is even not mentioned in spec. Is there any way to get a similar functionality with rambda?

dr_leevsey
  • 365
  • 4
  • 15

2 Answers2

2

modules

Here is a module-oriented approach. The following technique is a pattern all modern JavaScript developers will need to become familiar with. The benefits of modules are numerous, including -

  • Highly reusable code
  • Easy to test
  • Easy to refactor
  • Tree shakeable, ie dead code elimination

Given some data -

import { sum, prop } from "./Compare.js"

const data =
  [ { name: 'Alicia', age: 10 }
  , { name: 'Alice', age: 15 }
  , { name: 'Alice', age: 10 }
  , { name: 'Alice', age: 16 }
  ]

data.sort(sum(prop("name"), prop("age")))
console.log(data)
[ { name: 'Alice', age: 10 }
, { name: 'Alice', age: 15 }
, { name: 'Alice', age: 16 }
, { name: 'Alicia', age: 10}
]

With Compare module -

// Compare.js

import * as Ordered from "./Ordered.js"

const empty = 
  (a, b) => (a > b) ? Ordered.gt : (a < b) ? Ordered.lt : Ordered.eq

const map = (t, f) =>
  (a, b) => t(f(a), f(b))

const concat = (t1, t2) =>
  (a, b) => Ordered.concat(t1(a,b), t2(a,b))

const prop = (k, orElse) =>
  map(empty, o => o?.[k] ?? orElse)

const reverse = (t) =>
  (a, b) => t(b, a)

const sum = (...ts) =>
  ts.reduce(concat, empty)

export { empty, map, concat, prop, reverse, sum }

Which depends on Ordered module -

// Ordered.js

const eq = 0

const gt = 1

const lt = -1

const empty = eq

const concat = (t1, t2) =>
  t1 == eq ? t2 : t1

export { eq, gt, lt, concat, empty }

functional principles

Our Comparison module is flexible yet reliable. This allows us to write our sorters in a formula-like way -

// this...
concat(reverse(prop("name")), reverse(prop("age")))

// is the same as...
reverse(concat(prop("name"), prop("age")))

And similarly with concat expressions -

// this...
concat(prop("year"), concat(prop("month"), prop("day")))

// is the same as...
concat(concat(prop("year"), prop("month")), prop("day"))

// is the same as...
sum(prop("year"), prop("month"), prop("day"))

demo

Unfortunately we cannot directly test modules in StackSnippet answers. Below we implement Module for the sole purpose of embedding a demo on this page. I've been working with callcc lately, which is an unexpected but perfect fit -

const callcc = f => {
  const box = Symbol()
  try { return f(unbox => { throw {box, unbox} }) }
  catch (e) { if (e?.box == box) return e.unbox; throw e  }
}

const Module = callcc

const Ordered = Module(expose => {
  const eq = 0
  const gt = 1
  const lt = -1
  const empty = eq
  const concat = (t1, t2) =>
    t1 == eq ? t2 : t1
  expose({ eq, gt, lt, concat, empty })
})

const Compare = Module(expose => {
  const empty = 
    (a, b) => (a > b) ? Ordered.gt : (a < b) ? Ordered.lt : Ordered.eq
  const map = (t, f) =>
    (a, b) => t(f(a), f(b))
  const concat = (t1, t2) =>
    (a, b) => Ordered.concat(t1(a,b), t2(a,b))
  const prop = (k, orElse) =>
    map(empty, o => o?.[k] ?? orElse)
  const sum = (...ts) =>
    ts.reduce(concat, empty)
  expose({ empty, map, concat, prop, sum })
})

const data =
  [ { name: 'Alicia', age: 10 }
  , { name: 'Alice', age: 15 }
  , { name: 'Alice', age: 10 }
  , { name: 'Alice', age: 16 }
  ]

console.log(
  data.sort(Compare.sum(Compare.prop("name"), Compare.prop("age")))
)
.as-console-wrapper { min-height: 100%; top: 0; }

optimized Compare.sum

Given many comparisons, if a single comparison returns gt or lt, .sort already knows enough information to move the element. There's no need to continue reduce-ing the comparisons. However, the semantics of reduce say that it will run once for each element of the input array. Is there a way to short-circuit and provide an early return?

// Compare.js
const concat = (t1, t2) =>
  (a, b) => Ordered.concat(t1(a,b), t2(a,b))

const sum = (...ts) =>
  ts.reduce(concat, empty) // ⚠️
// Ordered.js
const concat = (t1, t2) =>
  t1 == eq ? t2 : t1

It turns out callcc can do exactly that for us. How convenient that I introduced it above! This optimized sum cannot be written in terms of concat and has a form much closer to @Scott's wonderful 1-liner. It somewhat steps on the Ordered module's toes but has advantage that it immediately stops comparing once the answer is known. For a significantly large input with complex comparisons, the performance increase is terrific -

// Compare.js

const sum = (...ts) =>
  (a, b) => callcc(exit => // ✅ early exit mechanism
    ts.reduce((o, t) => o ? exit(o) : t(a, b), Ordered.eq)
  )

read on

See these modules in action in some other posts I wrote:

Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    While I knew I could add an early escape mechanism to my solution at the cost of some code complexity, I didn't bother digging deeper. This is fascinating, not least in the use of `callcc` for `Module`. Hmmm – Scott Sauyet Apr 29 '22 at 18:46
  • @ScottSauyet interesting, right? i can actually think of a few uses cases for .`forEach` when paired with `callcc`, and would otherwise never use it. let me know if anything fun comes up on your end :D – Mulan Apr 29 '22 at 19:30
1

I don't know Rambda well. (Disclaimer: I'm a Ramda (no-b!) founder.) But it looks like they're open to pull requests, so you could try adding this yourself.

This is not a hard function to write for your own usage. Here's an (untested) version:

const sortWith = (fns) => (xs) => 
  [...xs] .sort ((a, b) => fns .reduce ((c, fn) => c || fn (a, b), 0))

Or look at Ramda's version, if you'd rather. Feel free to steal it, although the more modern version above is simpler.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    perfect 1-liner! see my answer to see how `callcc` can turbo charge `.reduce` in this unique scenario :D – Mulan Apr 29 '22 at 17:17