'rambda is missing sortWith
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?
Solution 1:[1]
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:
Solution 2:[2]
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.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | |
Solution 2 | Scott Sauyet |