1

I have some data in the form:

const data = {
    list: [1, 2, 3],
    newItem: 5
}

I want to make a function that appends the value of newItem to list resulting in this new version of data:

{
list: [1,2,3,5],
newItem: 5,
}

(Ultimately, I'd remove newItem after it's been moved into the list, but I'm trying to simplify the problem for this question).

I'm trying to do it using pointfree style and Ramda.js, as a learning experience.

Here's where I am so far:

const addItem = R.assoc('list',
    R.pipe(
        R.prop('list'),
        R.append(R.prop('newItem'))
    )
)

The idea is to generate a function that accepts data, but in this example the call to R.append also needs a reference to data, I'm trying to avoid explicitly mentioning data in order to maintain Pointfree style.

Is this possible to do without mentioning data?

Nathan Manousos
  • 13,328
  • 2
  • 27
  • 37

3 Answers3

3

If I understand correctly you want to go from {x:3, y:[1,2]} to [1,2,3]. Here's one way:

const fn = compose(apply(append), props(['x', 'y']))

fn({x:3, y:[1,2]});
//=> [1,2,3]
customcommander
  • 17,580
  • 5
  • 58
  • 84
  • Hmm, given OP's code, I assumed the expected output to be `{ "list": [ 1, 2, 3, 5 ], "newItem": 5 }` but your assumption seems to be a lot more logical. I'm not sure why the `assoc()` is used, though - might be a mistake – VLAZ Feb 22 '22 at 20:39
  • Ohhh I completely overlooked `assoc`! Fair comment; I'll see if I can come up with a reasonable alternative solution. – customcommander Feb 22 '22 at 20:54
  • I've got one and I'd post it. OP can pick which fits, I suppose. – VLAZ Feb 22 '22 at 20:59
  • Posting both now, myself... :-) – Scott Sauyet Feb 22 '22 at 21:00
  • I sort of ignored this answer because of the discussion VLAZ raised, but this is also an interesting approach. I do use `apply` on occasion, but it's really quite useful. `unapply` is as well, although perhaps less often. – Scott Sauyet Feb 22 '22 at 21:25
  • I wanted to have the updated list attached to the object, not on its own (as VLAZ guessed). I edited my question to state explicitly how I want the resulting data to look. Sorry, that was unclear. – Nathan Manousos Feb 22 '22 at 23:14
  • Thanks for this answer, although it doesn't solve my intended question, it helps me understand how you might get certain values where they need to be in order to keep things pointfree, I'm having a lot of fun with this. – Nathan Manousos Feb 22 '22 at 23:20
3

As the discussion on the answer from customcommander shows, there are two different possible interpretations.

If you want to just receive [1, 2, 3, 5], then you can do it as customcommander does, or the way I would choose:

const fn1 = lift (append) (prop ('newItem'), prop ('list'))

But if you wanted something like {list: [1, 2, 3, 5], newItem: 5}, then you might use the above inside applySpec and combine that with a merge, like this:

const fn2 = chain (mergeLeft, applySpec ({list: fn1}))

Here's a snippet:

const fn1 = lift (append) (prop ('newItem'), prop ('list')) 
const fn2 = chain (mergeLeft, applySpec ({list: fn1}))


const data = {list: [1, 2, 3], newItem: 5}

console .log (fn1 (data)) //=> [1, 2, 3, 5]
console .log (fn2 (data)) //=> {list: [1, 2, 3, 5], newItem: 5}
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>
<script> const {lift, append, prop, chain, mergeLeft, applySpec} = R </script>

This second one is a little unwieldy, once you inline fn1. It repeats the property list in two places, and that always bothers me. But I don't have a good solution at the moment.

I've several times wanted a combination of R.evolve and R.applySpec, which works on the outside like evolve, letting you specify only the properties which need to change, but whose transformation functions are given the whole input object, and not just the corresponding property.

With something like that, this might look like

const f3 = evolveSpec ({
  list: ({list, newItem}) => [...list, newItem]
})

or using the above:

const f3 = evolveSpec ({
  list: lift (append) (prop ('newItem'), prop ('list'))
})

I think this might be a useful candidate for inclusion in Ramda.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • 1
    Nice use of `lift` here. Love it <3 – customcommander Feb 22 '22 at 21:13
  • 1
    I really dig `lift` and am always finding uses for it. Perhaps it's just a golden hammer to me, but I think it's genuinely quite useful. – Scott Sauyet Feb 22 '22 at 21:16
  • 1
    It absolutely is. You could even use `propOr` if you needed some defaults. – customcommander Feb 22 '22 at 21:20
  • I spent a lot of time going over all the answers here and studying them yesterday, really mind-blowing stuff for me. This answer looks very elegant, but I still don't have a good understanding of how it works. I will keep coming back to it until I do, thanks! – Nathan Manousos Feb 23 '22 at 17:38
  • 2
    @NathanManousos: Also, I know this is a learning exercise for Ramda, and that you're specifically interested in point-free, but don't overlook the simple vanilla JS version: `({list, newItem, ...rest}) => ({list: list .concat (newItem), ... rest})` – Scott Sauyet Feb 23 '22 at 19:00
2

const addItem = R.chain
  ( R.assoc('list') )
  ( R.converge(R.append, [R.prop('newItem'), R.prop('list')]) );

const data = {
    list: [1, 2, 3],
    newItem: 5
};

console.log(addItem(data));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>

And here is why:

First we can have a look at what the current addItem is supposed to look like when not point free:

const addItem = x => R.assoc('list')
  (
    R.pipe(
        R.prop('list'),
        R.append(R.prop('newItem')(x))
    )(x)
  )(x);

console.log(addItem({ list: [1, 2, 3], newItem: 5 }));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>

It takes some data and uses it in three places. We can refactor a bit:

const f = R.assoc('list');
const g = x => R.pipe(
        R.prop('list'),
        R.append(R.prop('newItem')(x))
    )(x)

const addItem = x => f(g(x))(x);

console.log(addItem({ list: [1, 2, 3], newItem: 5 }));
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.28.0/ramda.min.js"></script>

The x => f(g(x))(x) part might not be obvious immediately but looking at the list of common combinators in JavaScript it can be identified as S_:

Name # Haskell Ramda Sanctuary Signature
chain S_³ (=<<)² chain² chain² (a → b → c) → (b → a) → b → c

Thus x => f(g(x))(x) can be simplified pointfree to R.chain(f)(g).


This leaves the g which still takes one argument and uses it in two places. The ultimate goal is to extract two properties from an object and pass them to R.append(), this can be more easily (and pointfree) be expressed with R.converge() as:

const g = R.converge(R.append, [R.prop('newItem'), R.prop('list')]);

Substituting the f and g back gives

const addItem = R.chain
     ( R.assoc('list') )
     ( R.converge(R.append, [R.prop('newItem'), R.prop('list')]) );
VLAZ
  • 26,331
  • 9
  • 49
  • 67
  • Ahh, that's nicer than my version! But I still prefer `lift (append) (prop ('newItem'), prop ('list'))` to the `converge` equivalent. – Scott Sauyet Feb 22 '22 at 21:12
  • @ScottSauyet I have to admit that I don't quite understand `lift` enough to use it :/ I always skip over it when choosing what I need. I should probably look over it more as I know it's powerful, but for some reason my brain refuses to accept it. Unlike other functions in Ramda where I read the docs and go "That works for me". – VLAZ Feb 22 '22 at 21:15
  • VLAZ: perhaps this will help: https://stackoverflow.com/a/36578013 – Scott Sauyet Feb 22 '22 at 21:17
  • That doesn't really talk about how it applies to functions as containers, but the basic idea there is that `a -> b` can be thought of as a container for `b`s and we lift some function which operates on values to one which operates on these functions as containers for values. When we supply the value to that function, it calls each of them to get new `b`s (or `c`s or `d`s -- they can be different types) and then supplies them to the lifted function, much the same way `converge` does. `lift` just happens to apply to many additional types. – Scott Sauyet Feb 22 '22 at 21:22
  • 1
    Oh, your answer there is amazing! Quite intuitive. I think I get it now. I'll have to play around with it more but now at least I know what situations I'd be using `lift` in and why it's useful. Many thanks again! – VLAZ Feb 22 '22 at 21:23
  • There's another great answer showing this as applied to functions and other types, either by David Chambers or Scott Christopher, but I can't find it at a quick look. – Scott Sauyet Feb 22 '22 at 21:27
  • I've been working through this and the other answers shared here for quite a while. While my first reaction was that this was hard to understand, I think I do understand it now. The version using `lift` also seems quite nice, but I also don't yet understand `lift`. I'll get there :) – Nathan Manousos Feb 22 '22 at 23:52
  • `lift` is more general than `converge` (although there are things `converge` can do that `lift` cannot.) But when it comes to working with functions. The only difference is syntax: `converge (foo, [bar, baz])` vs. `lift (foo) (bar, baz)`. – Scott Sauyet Feb 23 '22 at 18:53