zoukankan      html  css  js  c++  java
  • 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.

  • 相关阅读:
    python3 内置函数
    python3 模块和包
    python3 面向对象编程
    vue.js的devtools安装
    数组对象排序
    插件资源库
    Vue.js 2.0生命周期
    vue学习--自定义全局vue组件
    搭建VUE项目
    入职新公司
  • 原文地址:https://www.cnblogs.com/cuishengli/p/15208761.html
Copyright © 2011-2022 走看看