0

I want to apply different functions to some object properties. Lets say I have this object:

const person = {
  name: 'John',
  age: 30,
  friends: [],
};

and i have some functions that i want to apply to those properties:

const upperCase = str => str.toUpperCase() //for the name

const add10 = int => int + 10 // for the age

const addFriend = (value,list) => [...list,value] // adding a friend

and this should be the result:

const person = {
  name: 'JOHN',
  age: 40,
  friends: ['Lucas']
}

What is the best way to achieve this using functional programming and point free, and if you could include examples using Ramda I would appreciate it, Thanks!

customcommander
  • 17,580
  • 5
  • 58
  • 84
  • 2
    call each function on the member of the object. – Maihan Nijat Dec 16 '19 at 17:29
  • 1
    Ramda and Folktale are *wildly* different in their approach to functional programming. It's not that they are about "different" functional programming but they are about different *aspects* of it. Ramda gives you functional style helper functions like `R.map` or `R.flip` or `R.compose`, etc. While Folktale implements the algebraic types like Functor, Monad, Applicative, Grupoid, etc. The two aren't really interchangeable but can be used together. – VLAZ Dec 16 '19 at 17:32
  • @MaihanNijat i know i can call each function on the object, one at a time, but is there a way i can apply all those at the same time, and keep the functions pure? – David Sttivend Angel Dec 16 '19 at 17:34
  • @DavidSttivendAngel create a wrapper function for all those functions and call it only once. – Maihan Nijat Dec 16 '19 at 17:36
  • @MaihanNijat i think i know how to do it with regular calls. I updated the questions so the answer also includes point free. Thanks – David Sttivend Angel Dec 16 '19 at 17:51

2 Answers2

4

With Ramda, you're looking at the evolve function:

Creates a new object by recursively evolving a shallow copy of object, according to the transformation functions. All non-primitive properties are copied by reference.

You probably need to define "transform" functions on the fly; I bet you don't want to add 'Lucas' as a friend of every single person. Hopefully this is enough to get you started.

const transform =
  evolve(
    { name: toUpper
    , age: add(10)
    , friends: prepend('Lucas')
    });
    
    
console.log(

  transform(person)

);
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.26.1/ramda.min.js"></script>
<script>const {evolve, toUpper, add, prepend} = R;</script>
<script>
const person =
  { name: 'John'
  , age: 30
  , friends: []
  };
</script>

Point-free style is not the only way

⚠️

Please note that I've been able to implement this point-free style because I could hardcode each transformation functions. If you need to craft a transformation object on the fly, doing so point-free style is likely to be unreadable quickly.

customcommander
  • 17,580
  • 5
  • 58
  • 84
3

As usual, customcommander gives an excellent answer. Ramda's evolve is designed for exactly that and, in the simple case, can allow for an entirely point-free answer.

One of Ramda's authors, I very much like its functions. But it should be noted that it's quite easy to build your own versions of many, including this one, especially if you're in the simple situation where you only care about updating keys at the root of your object. (Ramda's recurs into nested spec objects, which is at least a bit more involved.)

So, if you didn't have Ramda handy, you might easily write a custom version of this function like this:

const evolve = (spec, keys = Object .keys (spec)) => (obj) => 
  Object .entries (obj) .reduce ( 
    (a, [k, v]) => ({...a, [k]: keys .includes (k) ? spec [k] (v) : v})
    , {}
  )

const upperCase = str => str.toUpperCase() //for the name
const add10 = int => int + 10 // for the age
const addFriend = (value) => (list) => [...list,value] // adding a friend
const person = {name: 'John', eys: 'blue', age: 30, friends: [], hair: 'red'}

console .log (
  evolve ({name: upperCase, age: add10, friends: addFriend('Lucas')}) (person)
)

This is far from the most performant code. An excellent article from Rich Snapp explains why and how to fix it. But I personally would stick with this unless this becomes a bottleneck in my application.

Of course, as customcommander also points out, there are Ramda functions for each of your helpes: toUpper, add, append (closer to the above implementation than prepend.)

A more complete discussion on the topic VLAZ raised in a comment can be found in https://stackoverflow.com/a/33130194/1243641.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • It's not just the reduce-spread-antipattern, also `concat` used as `push`/`unshift` is harmful, because it treats arrays as if they were persistent data structures. –  Dec 17 '19 at 09:00
  • @bob: But unless it becomes an actual performance problem for me, I will accept that as a trade-off in order to work -- as much as possible -- with expressions rather than statements. (Or do you have an expression-base approach that avoids this?) There are pretty good tools to tell me that something like this is a bottleneck in my application. But it's been several years since I even had to use one. Even writing in this style, I usually meet my speed goals and the slow-downs are almost always due to IO, and not such constructs. Perhaps there will come a day when I curse myself for it. – Scott Sauyet Dec 17 '19 at 13:03
  • The performance penalty is probably negligible in 99% of cases. However, I consider it good engineering to treat arrays as imperative data structures, i.e. local mutations are not only OK but the default. I use a couple of library functions that allow mutations and are still composable. It's a tradeoff as always. With `Object`s it is different, because they are inherently persistent when used with lenses.. –  Dec 17 '19 at 13:26
  • @bob: I don't follow your rationale for the different treatment of arrays and objects. I use lenses with both. I certainly use mutation of data structures within function boundaries at times, but then usually end up feeling a little dirty. The more I try incorporate expression-instead-of-statement code, the more likely I am to accept even this ill-performant pattern unless I see measurable harm. – Scott Sauyet Dec 17 '19 at 16:59
  • An `Object` is inherently persistent when used with lenses because only the property itself and its root path is created on updates whereas everything else is shared. With arrays the entire `Array` have to be created even though only a single element is updated. This is because arrays are inherently non-persistent. Does this make any sense? Hopefully. –  Dec 17 '19 at 17:29
  • @bob: I'm curious how you achieve that inherent persistence in JS. The lens implementation I know best (Ramda's) does not do so. It builds a new object up, reusing whatever parts it can, but creating new nodes along the whole path leading to the value. You get a whole new object, with as many shared references as possible. But arrays are fairly similar, with all nodes but the updated one sharing references with the original. While you could do something different using prototype chains or Proxies, I can't imagine using that for production work. I'm afraid I'm not making sense out of it. – Scott Sauyet Dec 17 '19 at 17:43
  • Yes, shared references. With arrays you always have to create the entire array because there is no real tree, just elements. With objects, however, there is a chance that copying the path from root to updated leaf doesn't mean to copy the whole object tree. If you use an object merely as an dictionary it isn't distinct from an array, of course. –  Dec 17 '19 at 19:00