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

时间:2018-09-30 19:10:59

标签: javascript functional-programming lodash immutable.js pointfree

我遇到了一种模式,我认为这可能是某种反模式,或者也许只有一种更好的实现方式。

请考虑以下实用程序功能,该功能可以重命名对象中的键,类似于使用终端命令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函数传递到另一个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迭代器功能都会使累加器发生变异,但前提是它只能在内部使用。在这里,数据集作为累加器参数从reducer传递到reducer,因为在仅包含几个要对数据集执行操作的列表的大型数据集上进行迭代没有意义。只要reducer iteratee函数(例如mv)不会使累加器发生突变,这种模式是否有问题,或者我是否缺少简单的东西?


基于@tokland的回答,我重写了测试以使用Immutable.js来查看确保不变性和潜在性能提升是否值得付出努力。互联网上有一些关于Immutable.js不适用于无点样式功能编程的文章。这是有道理的;但是,不多。据我所知,所有要做的就是编写一些基本函数来调用要使用的方法,例如mapfilterreduce。仍然可以使用不处理Javascript数组或对象的Lodash函数;换句话说,处理诸如currypipe之类的函数或处理诸如upperCase之类的字符串的Lodash函数似乎很好。

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

打字稿似乎在处理高阶函数时遇到一些问题,因此如果您正在使用// @ts-ignore进行测试,则必须将pipe放在tsc之前。

3 个答案:

答案 0 :(得分:1)

您的方法没有错。有时您将输入对象折叠起来,有时将其用作初始累加器,这取决于算法。如果化简器对函数调用者传递的值进行了更改,则无论何时需要不变性,都不能使用该化简器。

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

答案 1 :(得分:1)

基于@tokland的回答,我重写了测试以使用Immutable.js来查看确保不变性和潜在性能提升是否值得付出努力。互联网上有一些关于Immutable.js不适用于无点样式功能编程的文章。这是有道理的;但是,不多。据我所知,所有要做的就是编写一些基本函数来调用要使用的方法,例如mapfilterreduce。仍然可以使用不处理Javascript数组或对象的Lodash函数;换句话说,处理诸如currypipe之类的函数或处理诸如upperCase之类的字符串的Lodash函数似乎很好。

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

打字稿似乎在处理高阶函数时遇到一些问题,因此如果您正在使用// @ts-ignore进行测试,则必须将pipe放在tsc之前。

答案 2 :(得分:0)

我认为您的问题的答案是肯定的。我的意思是,在函数式编程中,纯函数是一件事,您正在尝试以函数式的方式进行操作,但会改变输入。因此,我认为您需要考虑使用类似于lodash/fp的{​​{3}}:

  

尽管lodash / fp及其方法模块已预先转换,但仍有   您可能需要自定义转化的时间。那时   转换方法派上用场。

     

//默认情况下,每个选项均为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
});

注意那里的immutable转换器。因此,这是我的答案的yes部分……但是no部分将是您仍然需要使用immutable方法作为默认设置才能真正实现纯功能。 / p>