以累加器作为最终参数的无点归约函数 - 函数式编程 - 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 不适合点自由风格函数式编程的喧嚣。这是有道理的;然而,并不多。据我所知,您所要做的就是编写一些调用您要使用的方法的基本函数,例如 map
、filter
、reduce
。不处理 Javascript 数组或对象的 Lodash 函数仍然可以使用;换句话说,处理函数的 Lodash 函数,如 curry
和 pipe
,或处理字符串,如 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 不适合无点式函数式编程的喧嚣。这是有道理的;然而,并不多。据我所知,所有需要做的就是编写一些调用您要使用的方法的基本函数,例如 map
、filter
、reduce
。不处理 Javascript 数组或对象的 Lodash 函数仍然可以使用;换句话说,处理函数的 Lodash 函数,如 curry
和 pipe
,或处理字符串,如 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
。
我已经 运行 进入了一种我认为可能是某种反模式的模式,或者也许有更好的方法来完成。
考虑以下重命名对象中键的实用函数,类似于使用终端命令重命名文件 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 不适合点自由风格函数式编程的喧嚣。这是有道理的;然而,并不多。据我所知,您所要做的就是编写一些调用您要使用的方法的基本函数,例如 map
、filter
、reduce
。不处理 Javascript 数组或对象的 Lodash 函数仍然可以使用;换句话说,处理函数的 Lodash 函数,如 curry
和 pipe
,或处理字符串,如 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 不适合无点式函数式编程的喧嚣。这是有道理的;然而,并不多。据我所知,所有需要做的就是编写一些调用您要使用的方法的基本函数,例如 map
、filter
、reduce
。不处理 Javascript 数组或对象的 Lodash 函数仍然可以使用;换句话说,处理函数的 Lodash 函数,如 curry
和 pipe
,或处理字符串,如 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
。