Thinking in Ramda: Pointfree Style

Thinking in Ramda: Pointfree Style

This post is Part 5 of a series about functional programming called Thinking in Ramda.

In Part 4, we talked about writing code declaratively (telling the computer what to do) instead of imperatively (telling the computer how to do it).

You may have noticed that several of the functions we’ve written (forever21, drivingAge, and water, for example) all take a parameter, build up a new function, and then apply that function to the parameter.

This is a very common pattern in functional programming, and once again Ramda provides the tools to clean this up.

Pointfree Style

There are two main guiding principles of Ramda that we talked about in Part 3:

  • Put the data last
  • Curry all the things

These two principles lead to a style that functional programmers call “pointfree”. I like to think of pointfree code as “Data? What data? There’s no data here.”

There’s a great blog post, Why Ramda?, that illustrates pointfree style really well. Tellingly, it has section headings like “Where’s the Data?”, “All Right, Already! May I See Some Data?”, and “I Just Want My Data, Thanks”.

We don’t yet have the tools we need to make all of our examples completely pointfree, but we can start.

Let’s look at forever21 again:

forever21

const forever21 = age => ifElse(gte(__, 21), always(21), inc)(age)

Notice that age only appears twice: once in the argument list, and once at the very end of the function as we apply the new function returned by ifElse to it.

If we pay attention while working with Ramda, we’ll see this pattern a lot. It almost always means that there’s a way to convert the function to pointfree style.

Let’s see what that would look like:

Pointfree forever21

const forever21 = ifElse(gte(__, 21), always(21), inc)

And, poof! We just made the age disappear. Pointfree style. Note that there is no behavioral difference in these two versions. We’re still returning a function that takes an age, but now we’re not explicitly specifying the age parameter.

We can do the same thing with alwaysDrivingAge and water as well.

When we left off, alwaysDrivingAge looked like this:

Original alwaysDrivingAge

const alwaysDrivingAge = age => ifElse(lt(__, 16), always(16), identity)(age)

We can apply the same transformation to make it pointfree.

Pointfree alwaysDrivingAge

const alwaysDrivingAge = when(lt(__, 16), always(16))

And here’s where we left water:

Using cond

const water = temperature => cond([
  [equals(0),   always(‘water freezes at 0°C‘)],
  [equals(100), always(‘water boils at 100°C‘)],
  [T,           temp => `nothing special happens at ${temp}°C`]
])(temperature)

And here it is, pointfree style:

Using cond

const water = cond([
  [equals(0),   always(‘water freezes at 0°C‘)],
  [equals(100), always(‘water boils at 100°C‘)],
  [T,           temp => `nothing special happens at ${temp}°C`]
])

Multi-argument Functions

What about functions that take more than one argument? Let’s look back at the titlesForYear example from Part 3.

titlesForYear

const titlesForYear = curry((year, books) =>
  pipe(
    filter(publishedInYear(year)),
    map(book => book.title)
  )(books)
)

Notice that books only appears twice: once as the last parameter in the argument list (data last!), and once at the very end of the function as we apply our pipeline to it. This is similar to the pattern we saw with age above, so let’s apply the same transformation to it:

Pointfree titlesForYear

const titlesForYear = year =>
  pipe(
    filter(publishedInYear(year)),
    map(book => book.title)
  )

It works! We now have a pointfree version of titlesForYear.

Honestly, I probably wouldn’t aim for pointfree style in this case because JavaScript doesn’t make it convenient to call a series of single-argument functions, as we discussed in earlier posts.

If we want to use titlesForYear in a pipeline, we’re fine. We can call titlesForYear(2012) very easily. But if we want to use it by itself, we have to go back to the )( pattern we saw in the previous post: titlesForYear(2012)(books). To me, that’s not worth the tradeoff.

But any time I have a single-argument function that follows (or can be refactored to follow) the pattern above, I’ll almost always make it pointfree.

Refactoring to Pointfree

There will be times when our functions don’t follow the pattern. We might be operating on the data multiple times in the same function.

This was the case in several of the examples in Part 2. In those examples, we refactored our code to combine functions using things like both, either, pipe, and compose. Once we’d done that, making our functions pointfree was a relatively easy transformation.

Let’s look back at the isEligibleToVote example. Here’s where we started:

Eligible Voters

const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY
const wasNaturalized = person => Boolean(person.naturalizationDate)
const isOver18 = person => person.age >= 18
const isCitizen = person => wasBornInCountry(person) || wasNaturalized(person)
const isEligibleToVote = person => isOver18(person) && isCitizen(person)

Let’s start with isCitizen. It takes a person and then applies two different functions to that person, combining the results with ||. As we learned in Part 2, we can instead use either to combine the two functions into a new function first, and then apply the combined function to the person.

Using either

const isCitizen = person => either(wasBornInCountry, wasNaturalized)(person)

We can do the same thing with isEligibleToVote using both:

Using both

const isEligibleToVote = person => both(isOver18, isCitizen)(person)

Now that we’ve done these refactorings, we can see that both functions follow the pattern we talked above above: person is mentioned twice, once as the function argument, and once at the end as we apply our combined function to it. We can now refactor to pointfree style:

With pointfree style

const isCitizen = either(wasBornInCountry, wasNaturalized)
const isEligibleToVote = both(isOver18, isCitizen)

Why?

Pointfree style takes time to get used to. It can be hard to adapt to the missing data arguments everywhere. It is also important to have some familiarity with Ramda’s functions to know how many arguments they eventually need.

But once you get used to it, it becomes very powerful to have a bunch of small pointfree functions combined together in interesting ways.

What’s the advantage of pointfree style? One could argue that it’s just an academic exercise designed to win a functional programming merit badge. However, I think there are a few advantages, even in spite of the work it takes to get used to the style:

  • It makes programs simpler and more concise. This isn’t always a good thing, but it can be.
  • It makes algorithms clearer. By focusing only on the functions being combined, we get a better sense of what’s going on without the data arguments getting in the way.
  • It forces us to think more about the transformation being done than about the data being transformed.
  • It helps us think about our functions as generic building blocks that can work with different kinds of data, rather than thinking about them as operations on a particular kind of data. By giving the data a name, we’re anchoring our thoughts about where we can use our functions. By leaving the data argument out, it allows us to be more creative.

Conclusion

Pointfree style, also known as tacit programming, can make our code clearer and easier to reason about. By refactoring our code to combine all of our transformations into a single function, we end up with smaller building blocks that can be used in more places.

Next

In our examples, we haven’t been able to refactor everything to pointfree style. We still have code that is written in an imperative style. Most of this code is dealing with objects and arrays.

We need to find declarative ways of interacting with objects and arrays. And what about immutability? How do we manipulate objects and arrays in an immutable way?

The next post in this series, Immutability and Objects discusses how to work with objects in a functional and immutable way. The post after that, Immutability and Arrays does the same for arrays.

Thinking in Ramda: Pointfree Style

上一篇:子传父


下一篇:C#实现类似QQ的隐藏浮动窗体、消息闪动