zoukankan      html  css  js  c++  java
  • Thinking in Ramda: Immutability and Objects

    Thinking in Ramda: Immutability and Objects

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

    In Part 5, we talked about writing our functions in “pointfree” or “tacit” style where the main data argument to our function is not explicitly shown.

    At that time, we were unable to convert all of our functions to pointfree style because we were missing some tools. It’s now time to learn those tools.

    Reading Object Properties

    Let’s look back at the eligible voters example that we revisited in Part 5:

    Eligible Voters

    const wasBornInCountry = person => person.birthCountry === OUR_COUNTRY
    const wasNaturalized = person => Boolean(person.naturalizationDate)
    const isOver18 = person => person.age >= 18
    const isCitizen = either(wasBornInCountry, wasNaturalized)
    const isEligibleToVote = both(isOver18, isCitizen)
    

    As you can see, we’ve made isCitizen and isEligibleToVote pointfree, but we couldn’t do that with the first three functions.

    As we learned in Part 4, we can make the functions more declarative using equals and gte. Let’s start there.

    Using equals and gte

    const wasBornInCountry = person => equals(person.birthCountry, OUR_COUNTRY)
    const wasNaturalized = person => Boolean(person.naturalizationDate)
    const isOver18 = person => gte(person.age, 18)
    

    In order to make these functions pointfree, we need a way to build up a function that we can then apply to the person at the end. The problem is that we need to access properties on the person and the only way we know how to do that is imperatively.

    prop

    Fortunately, Ramda can help us out. It provides the prop function for accessing properties of an object.

    Using prop, we can turn person.birthCountry into prop('birthCountry', person). Let’s start with that.

    Using prop

    const wasBornInCountry = person => equals(prop('birthCountry', person), OUR_COUNTRY)
    const wasNaturalized = person => Boolean(prop('naturalizationDate', person))
    const isOver18 = person => gte(prop('age', person), 18)
    

    Wow, that looks a lot worse now. But let’s keep going with the refactoring. First, let’s swap the order of the arguments we’re passing to equals so that prop comes last. equals works the same with either order, so this is safe.

    Swap equals argument order

    const wasBornInCountry = person => equals(OUR_COUNTRY, prop('birthCountry', person))
    const wasNaturalized = person => Boolean(prop('naturalizationDate', person))
    const isOver18 = person => gte(prop('age', person), 18)
    

    Next, let’s use the curried nature of equals and gte to make new functions that we apply to the result of the prop calls.

    Apply Currying

    const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry', person))
    const wasNaturalized = person => Boolean(prop('naturalizationDate', person))
    const isOver18 = person => gte(__, 18)(prop('age', person))
    

    That’s still a bit worse, but let’s keep going anyway. Let’s take advantage of currying again with all of the prop calls.

    More Currying

    const wasBornInCountry = person => equals(OUR_COUNTRY)(prop('birthCountry')(person))
    const wasNaturalized = person => Boolean(prop('naturalizationDate')(person))
    const isOver18 = person => gte(__, 18)(prop('age')(person))
    

    Worse again. But now we see a familiar pattern. All three of our functions have the same shape as f(g(person)), and we know from Part 2 that this is equivalent to compose(f, g)(person).

    Let’s take advantage of that.

    Using compose

    const wasBornInCountry = person => compose(equals(OUR_COUNTRY), prop('birthCountry'))(person)
    const wasNaturalized = person => compose(Boolean, prop('naturalizationDate'))(person)
    const isOver18 = person => compose(gte(__, 18), prop('age'))(person)
    

    Now we’re getting somewhere. Now all three functions look like person => f(person). We know from Part 5 that we can make these functions pointfree.

    Finally Pointfree

    const wasBornInCountry = compose(equals(OUR_COUNTRY), prop('birthCountry'))
    const wasNaturalized = compose(Boolean, prop('naturalizationDate'))
    const isOver18 = compose(gte(__, 18), prop('age'))
    

    It wasn’t obvious when we started that our methods were doing two different things. They were both accessing a property of an object and performing some operation on the value of that property. This refactoring to pointfree style has made that very explicit.

    Let’s look at some more tools that Ramda provides for working with objects.

    pick

    Where prop reads a single property from an object and returns the value, pick reads multiple properties from an object and returns a new object with just those properties.

    For example, if wanted just the names and ages of a person, we could use

    pick(['name', 'age'], person)
    

    has

    If we just want to know if an object has a property without reading the value, we can use has for checking own properties, and hasIn for checking up the prototype chain:

    has('name', person)
    

    path

    Where prop reads a property from an object, path dives into nested objects. For example, we could access the zip code from a deeper structure as

    path(['address', 'zipCode'], person)
    

    Note that path is more forgiving than prop. path will return undefined if anything along the path (including the original argument) is null or undefined whereas prop will raise an error.

    propOr / pathOr

    propOr and pathOr are similar to prop and path combined with defaultTo. They let you provide a default value to use if the property or path cannot be found in the target object.

    For example, we can provide a placeholder when we don’t know a person’s name:

    propOr('<Unnamed>', 'name', person)
    

    Note that unlike prop, propOr will not raise an error if person is null or undefined; it will instead return the default value.

    keys / values

    keys returns an array containing the names of all of the own properties in an object. values returns the values of those properties. These functions can be useful when combined with the collection iteration functions we learned about in Part 1.

    Adding, Updating, and Removing Properties

    We now have lots of tools for reading from objects declaratively, but what about when we want to make changes?

    Since immutability is important, we don’t want to change objects directly. Instead, we want to return new objects that have been changed in the way we want.

    Once again, Ramda provides a lot of help for us.

    assoc / assocPath

    When programming imperatively, we could set or change the name of a person with the assignment operator:

    person.name = 'New name'
    

    In our functional, immutable world we use assoc instead:

    const updatedPerson = assoc('name', 'New name', person)
    

    assoc returns a new object with the added or updated property value, leaving the original object unchanged.

    There is also assocPath for updating a nested property:

    const updatedPerson = assocPath(['address', 'zipcode'], '97504', person)
    

    dissoc / dissocPath / omit

    What about deleting properties? Imperatively, we might want to say

    delete person.age
    

    In Ramda, we’d use dissoc:

    const updatedPerson = dissoc('age', person)
    

    dissocPath is similar, but works deeper into the object structure:

    dissocPath(['address', 'zipCode'], person)
    

    There is also omit, which can remove several properties at once.

    const updatedPerson = omit(['age', 'birthCountry'], person)
    

    Note that pick and omit are quite similar and complement each other nicely. They’re very handy for white-listing (keep only this set of properties using pick) or black-listing (get rid of this set of properties using omit).

    Transforming Properties

    We now know enough to work with objects in a declarative and immutable fashion. Let’s write a function, celebrateBirthday, that updates a person’s age on their birthday.

    celebrateBirthday

    const nextAge = compose(inc, prop('age'))
    const celebrateBirthday = person => assoc('age', nextAge(person), person)
    

    This is a pretty common pattern. Rather than updating a property to a known new value, we really want to transform the value by applying a function to the old value as we’ve done here.

    I don’t know of a good way to write this with less duplication and in pointfree style given the tools we know about.

    Ramda to the rescue once more with its evolve function. I introduced evolve in using-ramda-with-redux.

    evolve takes an object that specifies a transformation function for each property to be transformed. Let’s refactor celebrateBirthday to use evolve:

    Using evolve

    const celebrateBirthday = evolve({ age: inc })
    

    This code is saying to evolve the target object (not shown here because of pointfree style) by making a new object with the same properties and values, but whose age is obtained by applying inc to the original value of age.

    evolve can transform multiple properties at once and at multiple levels of nesting. The transformation object can have the same shape as the object being evolved and evolve will recursively traverse both structures, applying transformation functions as it goes.

    Note that evolve will not add new properties; if you specify a transformation for a property that doesn’t appear in the target object, evolve will just ignore it.

    I’ve found that evolve has quickly become a workhorse in my applications.

    Merging Objects

    Sometimes, you’ll want to merge two objects together. A common case is when you have a function that takes named options and you want to combine those options with a set of default options. Ramda provides merge for this purpose.

    Function with Options

    function f(a, b, options = {}) {
      const defaultOptions = { value: 42, local: true }
      const finalOptions = merge(defaultOptions, options)
    }
    

    merge returns a new object containing all of the properties and values from both objects. If both objects have the same property, the value from the second argument is used.

    Having the second argument win makes sense when using merge by itself, but less so in a pipeline situation. In that case, you’re often performing a series of transformations to an object, and one of those transformations is to merge in some new property values. In this case, you want the first argument to win.

    Simply using merge(newValues) in the pipeline will not do what you expect.

    For this situation, I typically define a utility function called reverseMerge. It can be written as

    const reverseMerge = flip(merge)
    

    Recall that flip reverses the first two arguments of the function it is applied to.

    merge performs a shallow merge. If the objects being merged both have a property whose value is a sub-object, those sub-objects will not be merged.

    See also mergeDeepLeft, mergeDeepRight, mergeDeepWith, mergeDeepWithKey

    Note that merge only takes two arguments. If you want to merge multiple objects into one, there is mergeAll that takes an array of the objects to be merged.

    See also mergeLeft, mergeRight, mergeWith, mergeWithKey

    Conclusion

    This has given us a nice set of tools for working with objects in a declarative and immutable way. We can now read, add, update, delete, and transform properties in objects without changing the original objects. And we can do these things in a way that works when combining functions.

    Next

    Now that we can work with objects in an immutable way, what about arrays? Immutability and Arrays shows us how.

  • 相关阅读:
    ORACLE删除当前用户下所有的表的方法
    解决Tomcat对POST请求文件上传大小的限制
    Windows下如何查看某个端口被谁占用
    javamail彻底解决中文乱码的方法
    Tomcat通过setenv.bat指定jdk和jre(相对路径)
    Linux nohup命令详解
    shell 重启java 程序
    jstack命令执行报错:Unable to open socket file: target process not responding or HotSpot VM not loaded
    ToStringBuilder.reflectionToString用法
    vue自定义指令+只能输入数字和英文+修改v-model绑定的值
  • 原文地址:https://www.cnblogs.com/cuishengli/p/15209525.html
Copyright © 2011-2022 走看看