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?

- 365
- 4
- 15
2 Answers
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:

- 129,518
- 31
- 228
- 259
-
1While 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
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.

- 49,207
- 4
- 49
- 103
-
1perfect 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