[Vuejs]-How should i sort a list inside an object in functional programming with different logic based on different condition

4👍

You have several problems here –

  1. safely accessing deeply nested data on an uncertain object
  2. sorting an array of complex data using functional techniques
  3. safely and immutably updating deeply nested data on an uncertain object

1. Using .map and .chain is low-level and additional abstraction should be added when syntax becomes painful –

const state =
  { tree:
      { type: "sequoia"
      , children: [ "a", "b", "c" ]
      }
  }

recSafeProp(state, [ "tree", "type" ])
  .getOrElse("?") // "sequoia"

recSafeProp(state, [ "tree", "foobar" ])
  .getOrElse("?") // "?"

recSafeProp(state, [ "tree", "children", 0 ])
  .getOrElse("?") // "a"

recSafeProp(state, [ "tree", "children", 1 ])
  .getOrElse("?") // "b"

recSafeProp(state, [ "tree", "children", 99 ])
  .getOrElse("?") // "?"

We can implement recSafeProp easily, inventing our own convenience function, safeProp, along the way –

const { Nothing, fromNullable } =
  require("data.maybe")

const recSafeProp = (o = {}, props = []) =>
  props.reduce // for each props as p
    ( (r, p) => // safely lookup p on child
        r.chain(child => safeProp(child, p))
    , fromNullable(o) // init with Maybe o
    )

const safeProp = (o = {}, p = "") =>
  Object(o) === o // if o is an object
    ? fromNullable(o[p]) // wrap o[p] Maybe
    : Nothing()  // o is not an object, return Nothing

2. The Comparison module. You seem to have familiarity with functional modules so let’s dive right in –

const { empty, map } =
  Comparison

const prioritySort =
  map(empty, record => record.priority || 0)

// or...
const prioritySort =
  map(empty, ({ priority = 0}) => priority)

myarr.sort(prioritySort)

If you want immutable sort –

const isort = ([ ...copy ], compare = empty) =>
  copy.sort(compare)

const newArr =
  isort(origArr, proritySort)

Here’s the Comparison module. It is introduced in this Q&A. Check it out there if you’re interested to see how to make complex sorters in a functional way –

const Comparison =
  { empty: (a, b) =>
      a < b ? -1
        : a > b ? 1
          : 0
  , map: (m, f) =>
      (a, b) => m(f(a), f(b))
  , concat: (m, n) =>
      (a, b) => Ordered.concat(m(a, b), n(a, b))
  , reverse: (m) =>
      (a, b) => m(b, a)
  , nsort: (...m) =>
      m.reduce(Comparison.concat, Comparison.empty)
  }

const Ordered =
  { empty: 0
  , concat: (a, b) =>
      a === 0 ? b : a
  }

Combining sorters –

const { empty, map, concat } =
  Comparison

const sortByProp = (prop = "") =>
  map(empty, (o = {}) => o[prop])

const sortByFullName =
  concat
    ( sortByProp("lastName")  // primary: sort by obj.lastName
    , sortByProp("firstName") // secondary: sort by obj.firstName
    )

data.sort(sortByFullName) // ...

Composable sorters –

const { ..., reverse } =
  Comparison

// sort by `name` then reverse sort by `age`&nbsp;&ndash;
data.sort(concat(sortByName, reverse(sortByAge)))

Functional prinicples –

// this...
concat(reverse(sortByName), reverse(sortByAge))

// is the same as...
reverse(concat(sortByName, sortByAge))

As well as –

const { ..., nsort } =
  Comparison

// this...
concat(sortByYear, concat(sortByMonth, sortByDay))

// is the same as...
concat(concat(sortByYear, sortByMonth), sortByDay)

// is the same as...
nsort(sortByYear, sortByMonth, sortByDay)

3. The lowest hanging fruit to reach for here is Immutable.js. If I find more time later, I’ll show a lo-fi approach to the specific problem.

👤Mulan

3👍

Your solution seems overkill to me.

Functional programming is about many things, and there is no concise definition, but if your main unit of work is the pure function and if you don’t mutate data, you’re well on the way to being a functional programmer. Using an Either monad has some powerful benefits at times. But this code looks more like an attempt to fit Either into a place where it makes little sense.

Below is one suggested way to write this code. But I’ve had to make a few assumptions along the way. First, as you discuss fetching data from a server, I’m assuming that this has to run in an asynchronous mode, using Promises, async-await or a nicer alternative like Tasks or Futures. I further assumed that where you mention mocked_firewall is where you are doing the actual asynchronous call. (Your sample code treated it as an object where you could look up results from a path; but I can’t really make sense of that for mimicking a real service.) And one more assumption: the fold(() => log(...), x => log(x)) bit was nothing essential, only a demonstration that your code did what it was supposed to do.

With all that in mind, I wrote a version with an object mapping type to functions, each one taking data and state and returning a new state, and with the central function fetchData that takes something like your mocked_firewall (or really like my alteration of that to return a Promise) and returns a function that accepts a state and returns a new state.

It looks like this:

// State-handling functions
const handlers = {
  tree: (data, state) => ({... state, tree: data}),
  table: (data, state) => ({
     ... state, 
     table: {... data, records: prioritySort (data .records)}
  })
}

// Main function
const fetchData = (firewall) => (state) => 
  firewall (state)
    .then ((data) => (handlers [data .type] || ((data, state) => state)) (data, state))


// Demonstrations
fetchData (mocked_firewall) ({path: 'foo', table: null, tree: null})
  .then (console .log) // updates the tree

fetchData (mocked_firewall) ({path: 'bar', table: null, tree: null})
  .then (console .log) // updates the table
  
fetchData (mocked_firewall) ({path: 'baz', table: null, tree: null})
  .then (console .log) // server returns type we can't handle; no update
  
fetchData (mocked_firewall) ({path: 'qux', table: null, tree: null})
  .then (console .log) // server returns no `type`; no update
.as-console-wrapper {min-height: 100% !important; top: 0}
<script> 
// Dummy functions -- only for demonstration purposes
const prioritySort = (records) =>
  records .slice (0) .sort (({priority: p1}, {priority: p2}) => p1 - p2)

const mocked_firewall = ({path}) => 
  Promise .resolve ({
    foo: {
           type: "tree", 
           children: [{
             type: "table", 
             records: [{id: 1, priority: 15}, {id: 2, priority: 3}]
           }]
         },
    bar: { 
           type: 'table', 
           records: [{id: 1, priority: 7}, {id: 2, priority: 1}, {id: 3, priority: 4}]
         },
    baz: {type: 'unknown', z: 45},
  } [path] || {})
</script>

You will notice that this does not alter the state; instead it returns a new object for the state. I see that this is tagged vue, and as I understand it, that is not how Vue works. (This is one of the reasons I haven’t really used Vue, in fact.) You could easily change the handlers to update the state in place, with something like tree: (data, state) => {state.tree = data; return state}, or even skipping the return. But don’t let any FP zealots catch you doing this; remember that the functional programmers are well versed in “key algorithmic techniques such as recursion and condescension.” 1.

You also tagged this ramda.js. I’m one of the founders of Ramda, and a big fan, but I see Ramda helping here only around the edges. I included for instance, a naive version of the prioritySort that you mentioned but didn’t supply. A Ramda version would probably be nicer, something like

const prioritySort = sortBy (prop ('priority'))

Similarly, if you don’t want to mutate the state, we could probably rewrite the handler functions with Ramda versions, possibly simplifying a bit. But that would be minor. For the main function, I don’t see anything that would be improved by using Ramda functions.

There is a good testability argument to be made that we should pass not just the firewall to the main function but also the handlers object. That’s entirely up to you, but it does make it easier to mock and test the parts independently. If you don’t want to do that, it is entirely possible to inline them in the main function like this:

const fetchData = (firewall) => (state) => 
  firewall (state)
    .then ((data) => (({
      tree: (data, state) => ({... state, tree: data}),
      table: (data, state) => ({
        ... state, 
        table: {...data, records: prioritySort(data .records)} 
      })
    }) [data .type] || ((data, state) => state)) (data, state))

But in the end, I find the original easier to read, either as is, or with the handlers supplied as another parameter of the main function.


1 The original quote is from Verity Stob, but I know it best from James Iry’s wonderful A Brief, Incomplete, and Mostly Wrong History of Programming Languages.

1👍

🎈 Short and simple way

In my example I use ramda, you need to compose some functions and voila ✨:

const prioritySort = R.compose(
  R.when(
    R.propEq('type', 'tree'),
    R.over(
      R.lensProp('children'),
      R.map(child => prioritySort(child))
    )
  ),
  R.when(
    R.propEq('type', 'table'),
    R.over(
      R.lensProp('records'),
      R.sortBy(R.prop('priority')),
    )
  ),
)

const fetchData = R.pipe(
  endpoint => fetch(`https://your-api.com/${endpoint}`, opts).then(res => res.json()),
  R.andThen(prioritySort),
)

fetchData(`tree`).then(console.log)
fetchData(`table`).then(console.log)

Check the demo

For inspection you can simple use function const log = value => console.log(value) || value

R.pipe(
  // some code
  log,
  // some code
)

it will log the piping value.

👤sultan

Leave a comment