PurelyFunctional.tv Newsletter 370: Refactoring: replace get, modify, set with update

Issue 370 - March 23, 2020 · Archives · Subscribe

Quarantine update 😷

Folks, I hope this isn't too personal, but I think more personal connection is called for these days. I have quarantined myself at home, with my two kids and wife. These are trying times. We are as safe as we can be under the circumstances. I hope you are safe, too. It's times like these that really focus us in on our values.

It looks like the best way to slow---and help stop---the spread of the disease is by social distancing. That basically means stay away from other people---including friends, family, and strangers. I urge you to stay home, except for groceries and medical needs, for as long as it takes. When you do have to leave, practice excessive hygiene to slow the spread to others.

I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.

I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let alone learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.

Stay healthy. Wash your hands. Stay inside. Take care of loved ones.

Clojure Tip 💡

Refactoring: replace get, modify, set with update

I'm writing a book called Grokking Simplicity which aims to bring functional programming out of the Ivory Tower and into industry on a large scale. I'm trying to teach a lot of the stuff we take for granted as Clojure programmers.

One thing we do all the time is use the functions update and update-in (a nested version of update) to easily modify a deeply nested value. We do it so much that sometimes we forget the pain of writing it out manually, withou t using update-in.

Here's what updating one value in a map looks like if you don't have update:

// give Eric a 7% raise
var person = {first: "Eric", last: "Normand", salary: "20000"};
person['salary'] = person['salary']*1.07;

Of course, that mutates the person object. Using a copy-on-write version to get immutability:

objectSet(person, 'salary', person['salary']*1.07);

// defined in a library
function objectSet(object, key, value) {
  var copy = Object.assign({}, object);
  copy[key] = value;
  return copy;

There's a little duplication ('salary' written twice), but it's not terrible. However, what if we wanted to apply this same pattern to a nested value?

// map of employee id to employee
var employees = {'001': eric, '002': jane, '003': francis};
// update Eric's salary (Employee id 001)
objectSet(employees, "001", objectSet(employees["001"], "salary",
employees["001"]["salary"] * 1.07));

Goodness! That's getting rough. Just imagine if it was three deep.

But we can recognize that objectSet is being called recursively. Let's write out an update function that handles the un-nested case.

function update(object, key, f) {
  return objectSet(object, key, f(object[key]));

Updating Eric's salary becomes:

update(person, 'salary', function(salary) { return salary * 1.07; });
// OR
update(person, 'salary', salary=>salary*1.07);

Now a nested version that takes an array of keys as the path.

function updateIn(object, path, f) {
  if(path.length === 0)
    return f(object);
  var first = path[0];
  path = arrayShift(path); // copy-on-write version of .shift()
  return objectSet(object, first, updateIn(object[first], path, f));

Then we can update the employee like this:

// old way
objectSet(employees, "001", objectSet(employees["001"], "salary",
employees["001"]["salary"] * 1.07));

// new way
updateIn(employees, ["001", "salary"], s=>s*1.07);

Like I said, we already use this in Clojure quite a bit. My hope was to show how much work using saves. The refactoring makes working with nested immutable data much easier.

Clojure Challenge 🤔

Last week's challenge

The challenge in Issue 369 was to calculate the depth of a nested collection. You can see them here.

I was struck by the variety of interpretations. For instance, I considered an empty collection to have depth 1, the same depth as a collection with one integer in it. Others considered empty collections to be depth 0. The more I think about it, the more both seem to make sense, depending on the context. Could the definition of depth depend on what you want to use it for?

You can leave comments on these submissions in the gist itself. Please leave comments! You can also hit the Subscribe button to keep abreast of the comments. We're all here to learn.

This week's challenge

Headline -> Hashtags

Imagine you work for a newspaper and you're job is to automatically post news articles to Twitter. Of course, you need hash tags. Write a function that takes the headline (as a string) and returns a list of hashtags. The hashtags should be the three longest words in the headline, ordered longest to shortest, and of course, with a # in front. If there are fewer than three words, use as many words as there are. If two words are of the same length, prefer the one that occurs closest to the beginning.

Examples from The Onion, America's finest news source.

(->hashtags "Violently Bored Americans Begin Looting Puzzle Stores")
;; => ("#violently" "#americans" "#looting")
(->hashtags "Trump Quietly Checks With Aides To Make Sure He’d Be Included In
Receiving $1,000 Government Checks")
;; => ("#government" "#receiving" "#included")
(->hashtags "Nation Demands More Slow-Motion Footage Of Running Basset
;; => ("#demands" "#footage" "#running")

Remember, hashtags are typically all lowercase.

Thanks to this site for the challenge idea.

As usual, please reply to this email and let me know what you tried. I'll collect them up and share them in the next issue. If you don't want me to share your submission, let me know.

Rock on! Eric Normand