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

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

我已经 运行 进入了一种我认为可能是某种反模式的模式,或者也许有更好的方法来完成。

考虑以下重命名对象中键的实用函数,类似于使用终端命令重命名文件 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)
})

这只是一个可以在任何地方使用的示例函数。接下来考虑一个大型数据集,其中可能有一小部分要重命名的键。

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)
})

所以现在我们进入我的问题的主题,它围绕着一个模式,在这个模式中,您可能有一个大数据集和许多类似于 mv 的不同操作的小列表要对所述数据集执行。设置一个无点管道以将数据集从一个 reduce 函数向下传递到下一个似乎是理想的;但是,每个都必须将数据集作为累加器参数传递,因为我们不是在数据集上迭代,而是在一个小的操作列表上迭代。

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)
})

我的问题是这是否是某种反模式,或者是否有更好的方法来实现相同的结果。令我惊愕的是,通常将 reducer 函数的累加器参数用作内部状态,并对数据集进行迭代;然而,这里是相反的。大多数 reducer iteratee 函数都会改变累加器,因为它只在内部使用。在这里,数据集作为累加器参数从 reducer 传递到 reducer,因为在大型数据集上迭代没有意义,其中只有几个操作列表要对数据集执行。只要 reducer iteratee 函数,例如 mv 不改变累加器,这种模式有什么问题还是我缺少一些简单的东西?


根据@tokland 的回答,我重写了测试以使用 Immutable.js 来查看不可变性的保证和潜在的性能提升是否值得付出努力。互联网上有一些关于 Immutable.js 不适合点自由风格函数式编程的喧嚣。这是有道理的;然而,并不多。据我所知,您所要做的就是编写一些调用您要使用的方法的基本函数,例如 mapfilterreduce。不处理 Javascript 数组或对象的 Lodash 函数仍然可以使用;换句话说,处理函数的 Lodash 函数,如 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 在处理高阶函数时似乎存在一些问题,因此如果您使用 tsc 进行测试,则必须在 pipe 之前抛出 // @ts-ignore

我认为你的问题的答案是肯定的和否定的。我的意思是,在函数式编程中,纯函数是一回事,您正在尝试以函数式方式进行操作,但会改变输入。所以我认为你需要考虑 convert approach 类似于 lodash/fp 的做法:

Although lodash/fp & its method modules come pre-converted, there are times when you may want to customize the conversion. That’s when the convert method comes in handy.

// Every option is true by default.

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
});

注意那里的 immutable 转换器。所以这是我的回答的 yes 部分......但是 no 部分是你仍然需要有一个 immutable 方法作为默认的 pure/functional .

你的方法没有问题。有时你折叠输入对象,有时你将它用作初始累加器,这取决于算法。如果 reducer 改变了函数调用者传递的值,那么只要需要不变性,就不能使用这个 reducer。

也就是说,您的代码可能存在性能问题,具体取决于对象的大小(输入、键映射)。每次更改密钥时,都会创建一个全新的对象。如果您发现这是一个问题,您通常会使用一些有效的不可变结构来重用输入数据(映射不需要,因为您不更新它们)。例如在 Map 来自 immutable.js.

根据@tokland 的回答,我重写了测试以使用 Immutable.js 来查看不可变性的保证和潜在的性能提升是否值得付出努力。互联网上有一些关于 Immutable.js 不适合无点式函数式编程的喧嚣。这是有道理的;然而,并不多。据我所知,所有需要做的就是编写一些调用您要使用的方法的基本函数,例如 mapfilterreduce。不处理 Javascript 数组或对象的 Lodash 函数仍然可以使用;换句话说,处理函数的 Lodash 函数,如 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 在处理高阶函数时似乎存在一些问题,因此如果您使用 tsc 进行测试,则必须在 pipe 之前抛出 // @ts-ignore