'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