简体   繁体   English

以累加器为最终参数的无点归约函数 - 函数式编程 - Javascript - Immutable.js

[英]Point-Free Reduce Function with Accumulator as Final Argument - Functional Programming - Javascript - Immutable.js

I've run into a pattern that I feel may be some sort of anti-pattern, or perhaps there's just a better way to accomplish.我遇到了一种模式,我觉得它可能是某种反模式,或者可能有更好的方法来完成。

Consider the following utility function that renames a key in an object similar to renaming a file with the terminal command mv .考虑以下实用程序函数,它重命名对象中的键类似于使用终端命令mv重命名文件。

import { curry, get, omit, pipe, set, reduce } from 'lodash/fp'

const mv = curry(
  (oldPath, newPath, source) =>
    get(oldPath, source)
      ? pipe(
          set(newPath, get(oldPath, source)),
          omit(oldPath)
        )(source)
      : source
)

test('mv', () => {
  const largeDataSet = { a: 'z', b: 'y', c: 'x' }
  const expected = { a: 'z', q: 'y', c: 'x' }
  const result = mv('b', 'q', largeDataSet)

  expect(result).toEqual(expected)
})

It's just an example function that can be used anywhere.这只是一个可以在任何地方使用的示例函数。 Next consider then a large data set that may have a small list of keys to rename.接下来考虑一个大数据集,它可能有一小部分要重命名的键。

test('mvMore', () => {
  const largeDataSet = { a: 'z', b: 'y', c: 'x' }
  const expected = { a: 'z', q: 'y', m: 'x' }
  const keysToRename = [['b', 'q'], ['c', 'm']]

  const result = reduce(
    (acc, [oldPath, newPath]) => mv(oldPath, newPath, acc),
    largeDataSet,
    keysToRename
  )

  expect(result).toEqual(expected)
})

So now we get to the subject of my question which revolves around a pattern where you may have a large data set and many small lists of different operations similar to mv to perform upon said data set.所以现在我们进入我的问题的主题,它围绕一种模式,在这种模式下,您可能拥有一个大型数据集和许多类似于mv的不同操作的小列表,以对所述数据集执行。 Setting up a point-free pipe to pass the data set down from one reduce function to the next seems ideal;设置一个无点管道将数据集从一个 reduce 函数传递到下一个似乎是理想的; however, each must pass the data set as the accumulator argument, becuase we are not iterating over the data set, but over a small lists of operations.然而,每个都必须将数据集作为累加器参数传递,因为我们不是在数据集上迭代,而是在一个小的操作列表上迭代。

test('pipe mvMore and similar transforms', () => {
  const largeDataSet = { a: 'z', b: 'y', c: 'x' }
  const expected = { u: 'z', r: 'y', m: 'x' }
  const keysToRename = [['b', 'q'], ['c', 'm']]
  const keysToRename2 = [['q', 'r'], ['a', 'u']]
  const mvCall = (source, [oldPath, newPath]) => mv(oldPath, newPath, source)
  const reduceAccLast = curry((fn, it, acc) => reduce(fn, acc, it))

  const result = pipe(
    // imagine other similar transform
    reduceAccLast(mvCall, keysToRename),
    // imagine other similar transform
    reduceAccLast(mvCall, keysToRename2)
  )(largeDataSet)

  expect(result).toEqual(expected)
})

My question is whether this is an anti-pattern of some sort, or if there is a better way to accomplish the same result.我的问题是这是否是某种反模式,或者是否有更好的方法来实现相同的结果。 What gives me consternation is that typically the accumulator argument of a reducer function is used as internal state and the data set is iterated over;让我感到震惊的是,reducer 函数的 accumulator 参数通常用作内部状态,并且数据集被迭代; however, here it is the other way around.然而,这里正好相反。 Most reducer iteratee functions will mutate the accumulator with the understanding that it is only being used internally.大多数reducer iteratee 函数都会改变累加器,因为它只在内部使用。 Here, the dataset is being passed from reducer to reducer as the accumulator argument because it does not make sense to iterate over a large data set where there are only lists of a few operations to perform on the data set.在这里,数据集作为累加器参数从 reducer 传递到 reducer,因为迭代大型数据集是没有意义的,其中只有少数操作的列表要对数据集执行。 As long as the reducer iteratee functions, eg, mv do not mutate the accumulator, is there any problem with this pattern or is there something simple I am missing?只要reducer iteratee 函数,例如, mv不改变累加器,这种模式是否有任何问题,或者我遗漏了什么简单的东西?


Based on @tokland's answer I rewrote the tests to use Immutable.js to see if the guarantees of immutability and the potential gain in performance were worth the effort.根据@tokland 的回答,我重写了测试以使用 Immutable.js 来查看不变性的保证和潜在的性能增益是否值得付出努力。 There was some hoopla on the internets about Immutable.js not being a good fit for point-free style functional programming.互联网上有一些关于 Immutable.js 不适合无点风格函数式编程的大肆宣传。 There is some truth to that;这是有道理的; however, not much.然而,不多。 From what I can tell, all one has to do is write a few basic functions that call the methods you want to use, eg, map , filter , reduce .据我所知,您所要做的就是编写一些基本函数来调用您要使用的方法,例如mapfilterreduce Lodash functions that do not deal with Javascript Arrays or Objects can still be used;不处理 Javascript 数组或对象的 Lodash 函数仍然可以使用; in otherwords, Lodash functions that deal with functions, like curry and pipe , or with strings, like upperCase seem to be fine.换句话说,处理函数(如currypipe )或字符串(如upperCase似乎没问题。

import { curry, pipe, upperCase } from 'lodash/fp'
import { Map } from 'immutable'

const remove = curry((oldPath, imm) => imm.remove(oldPath))
const get = curry((path, imm) => imm.get(path))
const set = curry((path, source, imm) => imm.set(path, source))
const reduce = curry((fn, acc, it) => it.reduce(fn, acc))
const reduceAcc = curry((fn, it, acc) => reduce(fn, acc, it))
const map = curry((fn, input) => input.map(fn))

const mv = curry((oldPath, newPath, source) =>
  pipe(
    set(newPath, get(oldPath, source)),
    remove(oldPath)
  )(source)
)

const mvCall = (acc, newPath, oldPath) => mv(oldPath, newPath, acc)

function log(x) {
  console.log(x)
  return x
}

test('mv', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ a: 'z', q: 'y', c: 'x' })
  const result = mv('b', 'q', largeDataSet)

  expect(result).toEqual(expected)
})

test('mvMore', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ a: 'z', q: 'y', m: 'x' })
  const keysToRename = Map({ b: 'q', c: 'm' })
  const result = reduce(mvCall, largeDataSet, keysToRename)

  expect(result).toEqual(expected)
})

test('pipe mvMore and similar transforms', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ u: 'Z', r: 'Y', m: 'X' })
  const keysToRename = Map({ b: 'q', c: 'm' })
  const keysToRename2 = Map({ q: 'r', a: 'u' })

  const result = pipe(
    reduceAcc(mvCall, keysToRename),
    reduceAcc(mvCall, keysToRename2),
    map(upperCase)
  )(largeDataSet)

  const result2 = keysToRename2
    .reduce(mvCall, keysToRename.reduce(mvCall, largeDataSet))
    .map(upperCase)

  expect(result).toEqual(expected)
  expect(result2).toEqual(expected)
})

Typescript seems to have some problems handling higher-order functions so gotta throw the // @ts-ignore s up before pipe if you're testing with tsc . Typescript 似乎在处理高阶函数时遇到了一些问题,因此如果您正在使用tsc进行测试,则必须在pipe之前抛出// @ts-ignore s。

There is nothing wrong with your approach.你的方法没有任何问题。 Sometimes you fold over the input object, sometimes you use it as the initial accumulator, it depends on the algorithm.有时您折叠输入对象,有时您将其用作初始累加器,这取决于算法。 If a reducer mutates a value passed by the function caller, then this reducer cannot be used whenever immutability is required.如果reducer 改变了函数调用者传递的值,那么无论何时需要不变性,都不能使用这个reducer。

That said, your code may have performance issues, depending on the size of the objects (input, key mappings).也就是说,您的代码可能存在性能问题,具体取决于对象的大小(输入、键映射)。 Every time you change a key, you create a brand new object.每次更改密钥时,都会创建一个全新的对象。 If you see that's a problem, you'd typically use some efficient immutable structure that reuses data for the input (not necessary for the mappings, since you do not update them).如果您发现这是一个问题,您通常会使用一些高效的不可变结构,为输入重用数据(映射不需要,因为您不更新它们)。 Look for example at Map from immutable.js.以 immutable.js 中的Map为例。

Based on @tokland's answer I rewrote the tests to use Immutable.js to see if the guarantees of immutability and the potential gain in performance were worth the effort.根据@tokland 的回答,我重写了测试以使用 Immutable.js 来查看不变性的保证和潜在的性能增益是否值得付出努力。 There was some hoopla on the internets about Immutable.js not being a good fit for point-free style functional programming.互联网上有一些关于 Immutable.js 不适合无点风格函数式编程的大肆宣传。 There is some truth to that;这是有道理的; however, not much.然而,不多。 From what I can tell, all one has to do is write a few basic functions that call the methods you want to use, eg, map , filter , reduce .据我所知,您所要做的就是编写一些基本函数来调用您要使用的方法,例如mapfilterreduce Lodash functions that do not deal with Javascript Arrays or Objects can still be used;不处理 Javascript 数组或对象的 Lodash 函数仍然可以使用; in otherwords, Lodash functions that deal with functions, like curry and pipe , or with strings, like upperCase seem to be fine.换句话说,处理函数(如currypipe )或字符串(如upperCase似乎没问题。

import { curry, pipe, upperCase } from 'lodash/fp'
import { Map } from 'immutable'

const remove = curry((oldPath, imm) => imm.remove(oldPath))
const get = curry((path, imm) => imm.get(path))
const set = curry((path, source, imm) => imm.set(path, source))
const reduce = curry((fn, acc, it) => it.reduce(fn, acc))
const reduceAcc = curry((fn, it, acc) => reduce(fn, acc, it))
const map = curry((fn, input) => input.map(fn))

const mv = curry((oldPath, newPath, source) =>
  pipe(
    set(newPath, get(oldPath, source)),
    remove(oldPath)
  )(source)
)

const mvCall = (acc, newPath, oldPath) => mv(oldPath, newPath, acc)

function log(x) {
  console.log(x)
  return x
}

test('mv', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ a: 'z', q: 'y', c: 'x' })
  const result = mv('b', 'q', largeDataSet)

  expect(result).toEqual(expected)
})

test('mvMore', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ a: 'z', q: 'y', m: 'x' })
  const keysToRename = Map({ b: 'q', c: 'm' })
  const result = reduce(mvCall, largeDataSet, keysToRename)

  expect(result).toEqual(expected)
})

test('pipe mvMore and similar transforms', () => {
  const largeDataSet = Map({ a: 'z', b: 'y', c: 'x' })
  const expected = Map({ u: 'Z', r: 'Y', m: 'X' })
  const keysToRename = Map({ b: 'q', c: 'm' })
  const keysToRename2 = Map({ q: 'r', a: 'u' })

  const result = pipe(
    reduceAcc(mvCall, keysToRename),
    reduceAcc(mvCall, keysToRename2),
    map(upperCase)
  )(largeDataSet)

  const result2 = keysToRename2
    .reduce(mvCall, keysToRename.reduce(mvCall, largeDataSet))
    .map(upperCase)

  expect(result).toEqual(expected)
  expect(result2).toEqual(expected)
})

Typescript seems to have some problems handling higher-order functions so gotta throw the // @ts-ignore s up before pipe if you're testing with tsc . Typescript 似乎在处理高阶函数时遇到了一些问题,因此如果您正在使用tsc进行测试,则必须在pipe之前抛出// @ts-ignore s。

I think the answer to your question is yes and no.我认为你的问题的答案是肯定的和否定的。 What I mean by that is in functional programming pure functions are a thing and your are trying to do it in a functional way but mutate the input.我的意思是在函数式编程中,纯函数是一回事,您正试图以函数式方式来实现,但会改变输入。 So I think you need to consider having a convert approach similar to how lodash/fp does it:因此,我认为您需要考虑采用类似于lodash/fp转换方法

Although lodash/fp & its method modules come pre-converted, there are times when you may want to customize the conversion.尽管 lodash/fp 及其方法模块已预先转换,但有时您可能希望自定义转换。 That's when the convert method comes in handy.这就是 convert 方法派上用场的时候。

// Every option is true by default. // 默认情况下每个选项都为true

var _fp = fp.convert({
  // Specify capping iteratee arguments.
  'cap': true,
  // Specify currying.
  'curry': true,
  // Specify fixed arity.
  'fixed': true,
  // Specify immutable operations.
  'immutable': true,
  // Specify rearranging arguments.
  'rearg': true
});

Notice the immutable converter there.注意那里的immutable转换器。 So this is the yes part of my answer ... but the no part would be that you still need to have a immutable approach as a default to be truly pure/functional.所以这是我答案的yes部分……但no部分是您仍然需要有一个immutable方法作为默认值才能真正实现纯/功能性。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM