5

A comparator function ascending accepts two arguments - a and b. It must return an integer comparing the two.

I have a list that I want to sort by name, so I wrote the following functions.

Is there a functional idiom I can use to combine these two functions, rather than having byName take responsibility for composing the resulting function?

const ascending = (a, b) => a.localeCompare(b);
const byName = (i) => i.get('name');
const useTogether = (...fns) => ...; // is there an idiomatic function like this?

// usage
items.sort(useTogether(byName(ascending))); 
Ben Aston
  • 53,718
  • 65
  • 205
  • 331
  • 2
    Just as an aside, sorting JavaScript arrays by string is best done with [String#localeCompare](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/localeCompare) so your sorting function would be `const ascendingString = (a, b) => a.localeCompare(b);` which wouldn't be combinable. – James Long Dec 04 '17 at 16:37
  • `const ascending = g => (a, b) =>((a,b) => a < b ? ... )(g(a), g(b));` and then `item.sort(ascending(by => by.name))` – Jonas Wilms Dec 04 '17 at 16:38
  • 1
    Take a look at [**thenBy.js**](https://github.com/Teun/thenBy.js). – ibrahim mahrir Dec 04 '17 at 16:46

2 Answers2

7

You're looking for contravariant functors

To appreciate them properly, let's start by examining the most basic sorting program

const compare = (a, b) =>
  a.localeCompare (b)

const data =
  [ 'Cindy'
  , 'Alice'
  , 'Darius'
  , 'Bertrand'
  ]
  
data.sort (compare)

console.log (data)
// Alice, Bertrand, Cindy, Darius

Nothing special there. Let's make our first contravariant functor, Comparison. The sort causes mutation, but it's just for demonstration anyway. Focus on contramap

const compare = (a, b) =>
  a.localeCompare (b)

const Comparison = (f = compare) =>
  ({ contramap : g =>
       Comparison ((a, b) => f (g (a), g (b)))
   , sort : xs =>
       xs.sort (f)
   })
   
const data =
  [ { name: 'Cindy' }
  , { name: 'Alice' }
  , { name: 'Darius' }
  , { name: 'Bertrand' }
  ]
  
Comparison ()
  .contramap (x => x.name)
  .sort (data)
  
console.log (data)
// Alice, Bertrand, Cindy, Darius

Composition law holds

m.contramap(f).contramap(g) == m.contramap(compose(f,g))

const compare = (a, b) =>
  a.localeCompare (b)

const Comparison = (f = compare) =>
  ({ contramap : g =>
       Comparison ((a, b) => f (g (a), g (b)))
   , sort : xs =>
       xs.sort (f)
   })

const data =
  [ { name: 'Cindy' }
  , { name: 'Alice' }
  , { name: 'Darius' }
  , { name: 'Bertrand' }
  ]

const compose = (f, g) =>
  x => f (g (x))

Comparison ()
  .contramap (compose (x => x.substr (1), x => x.name))
  // equivalent to
  // .contramap (x => x.substr (1)) // sort starting with second letter
  // .contramap (x => x.name)       // get name property
  .sort (data)
  
console.log (data)
// sorted by second letter this time (A, E, I, L)
// Darius, Bertrand, Cindy, Alice
//  ^       ^         ^      ^

Implementing the monoid interface gives you cool things like "multi-sort"

const Eq =
  0

const Lt =
  -1

const Gt =
  1

const Ord =
  { empty: Eq
  , concat: (a,b) =>
      a === Eq ? b : a
  }

const compare = (a, b) =>
  a < b ? Lt
    : a > b ? Gt
      : Eq

const Comparison = (f = compare) =>
  ({ compare: f
   , contramap : g =>
       Comparison ((a, b) => f (g (a), g (b)))
   , concat : m =>
       Comparison ((a, b) =>
         Ord.concat (f (a, b), m.compare (a, b)))   
   , sort : xs =>
       xs.sort (f)
   })
     
const data =
  [ { name: 'Alicia', age: 10 }
  , { name: 'Alice', age: 15 }
  , { name: 'Alice', age: 10 }
  , { name: 'Alice', age: 16 }
  ]

const sortByName =
  Comparison ()
    .contramap (x => x.name)
    
const sortByAge =
  Comparison ()
    .contramap (x => x.age)

sortByName
  .concat (sortByAge)
  .sort (data)
  
console.log ('sorted by (name,age)', data)
// Alice 10
// Alice 15
// Alice 16
// Alicia 10

sortByAge
  .concat (sortByName)
  .sort (data)

console.log ('sorted by (age,name)', data)
// Alice 10
// Alicia 10
// Alice 15
// Alice 16

Read the linked article for more useful information and an introduction to transducers

Mulan
  • 129,518
  • 31
  • 228
  • 259
5

I'm not sure if it satisfies what you're looking for, but here is one possible formulation of your useTogether (with a somewhat different signature) that does WORK. I'm not aware of a standard function with precisely that effect.

const ascending = (a, b) => a.localeCompare(b);
const byName = (i) => i['name'];
const useTogether = (selector, consumer) => (...fnArgs) => consumer(...fnArgs.map(selector));

var items = [{ name: "C" }, { name: "A" }, { name: "B" }];

console.log(
  items.sort(useTogether(byName, ascending))
)
nem035
  • 34,790
  • 6
  • 87
  • 99
S McCrohan
  • 6,663
  • 1
  • 30
  • 39