Skip to content
This repository was archived by the owner on Jan 6, 2025. It is now read-only.

Improve curry performances #317

Merged
merged 9 commits into from
Dec 10, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/immutadot-benchmark/src/benchmark.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ const benchmarkSuite = new BenchmarkSuite(
['immer', 'immer 1.8.0'],
['qim', 'qim 0.0.52'],
['immutadot', 'immutad●t 2.0.0'],
['qim-curried', 'qim 0.0.52 curried'],
['immutadot-curried', 'immutad●t 2.0.0 curried'],
],
)

Expand Down
12 changes: 12 additions & 0 deletions packages/immutadot-benchmark/src/setProp.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,16 @@ export function setProp(benchmarkSuite) {
return set(baseState, 'nested.prop', 'bar')
})
})

it('qim curried', () => {
benchmark('qim-curried', () => {
return qim.set(['nested', 'prop'])('bar')(baseState)
})
})

it('immutad●t curried', () => {
benchmark('immutadot-curried', () => {
return set('nested.prop')('bar')(baseState)
})
})
}
14 changes: 14 additions & 0 deletions packages/immutadot-benchmark/src/updateTodos.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,4 +113,18 @@ export function updateTodos(benchmarkSuite, title, listSize, modifySize, maxTime
return set(baseState, `[${start}:${end}].done`, true)
})
})

it('qim curried', () => {
benchmark('qim-curried', () => {
const [start, end] = randomBounds()
return qim.set([qim.$slice(start, end), qim.$each, 'done'])(true)(baseState)
})
})

it('immutad●t curried', () => {
benchmark('immutadot-curried', () => {
const [start, end] = randomBounds()
return set(`[${start}:${end}].done`)(true)(baseState)
})
})
}
11 changes: 7 additions & 4 deletions packages/immutadot/src/core/apply.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,13 @@ function apply(fn, { arity = fn.length, fixedArity = false, curried = true, lazy
)
}

if (curried)
// Add obj and path but remove value in arity
// TODO try improving curry when fixedArity is true
return curry(appliedFn, arity + 1)
if (curried) {
return curry(appliedFn, {
// Add obj and path but remove value in arity
arity: arity + 1,
fixedArity,
})
}

return appliedFn
}
Expand Down
90 changes: 79 additions & 11 deletions packages/immutadot/src/core/curry.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,85 @@
export function curry(fn, minArity = fn.length) {
function curried(prevArgs) {
return (...args) => {
if (prevArgs.length >= minArity - 1)
return fn(args[0], ...prevArgs)
const curried2 = fn => arg1 => arg0 => fn(arg0, arg1)

return curried(prevArgs.concat(args))
}
const curried32 = fn => (arg1, arg2) => arg0 => fn(arg0, arg1, arg2)
const curried31 = fn => arg1 => (...args) => {
if (args.length === 1) return curried32(fn)(arg1, args[0])
return fn(args[1], arg1, args[0])
}
const curried3 = fn => (...args) => {
if (args.length === 1) return curried31(fn)(...args)
return curried32(fn)(...args)
}

const curried43 = fn => (arg1, arg2, arg3) => arg0 => fn(arg0, arg1, arg2, arg3)
const curried42 = fn => (arg1, arg2) => (...args) => {
if (args.length === 1) return curried43(fn)(arg1, arg2, args[0])
return fn(args[1], arg1, arg2, args[0])
}
const curried41 = fn => arg1 => (...args) => {
if (args.length === 1) return curried42(fn)(arg1, args[0])
if (args.length === 2) return curried43(fn)(arg1, args[0], args[1])
return fn(args[2], arg1, args[0], args[1])
}
const curried4 = fn => (...args) => {
if (args.length === 1) return curried41(fn)(...args)
if (args.length === 2) return curried42(fn)(...args)
return curried43(fn)(...args)
}

const curried54 = fn => (arg1, arg2, arg3, arg4) => arg0 => fn(arg0, arg1, arg2, arg3, arg4)
const curried53 = fn => (arg1, arg2, arg3) => (...args) => {
if (args.length === 1) return curried54(fn)(arg1, arg2, arg3, args[0])
return fn(args[1], arg1, arg2, arg3, args[0])
}
const curried52 = fn => (arg1, arg2) => (...args) => {
if (args.length === 1) return curried53(fn)(arg1, arg2, args[0])
if (args.length === 2) return curried54(fn)(arg1, arg2, args[0], args[1])
return fn(args[2], arg1, arg2, args[0], args[1])
}
const curried51 = fn => arg1 => (...args) => {
if (args.length === 1) return curried52(fn)(arg1, args[0])
if (args.length === 2) return curried53(fn)(arg1, args[0], args[1])
if (args.length === 3) return curried54(fn)(arg1, args[0], args[1], args[2])
return fn(args[3], arg1, args[0], args[1], args[2])
}
const curried5 = fn => (...args) => {
if (args.length === 1) return curried51(fn)(...args)
if (args.length === 2) return curried52(fn)(...args)
if (args.length === 3) return curried53(fn)(...args)
return curried54(fn)(...args)
}

const curriedN = (fn, arity) => (...firstArgs) => {
const curried = (...prevArgs) => (...args) => {
if (prevArgs.length >= arity - 1)
return fn(args[0], ...prevArgs)
return curried(...prevArgs, ...args)
}
return curried(...firstArgs)
}

return (...args) => {
if (args.length >= minArity)
return fn(...args)
// FIXME doc with explanation of fixedArity's purpose and inconsistency
export function curry(fn, { arity = fn.length, fixedArity = false } = {}) {
let curried

return curried(args)
if (fixedArity) {
switch (arity) {
case 2:
curried = curried2(fn)
break
case 3:
curried = curried3(fn)
break
case 4:
curried = curried4(fn)
break
case 5:
curried = curried5(fn)
break
}
}

if (!curried) curried = curriedN(fn, arity)

return (...args) => args.length >= arity ? fn(...args) : curried(...args)
}
63 changes: 63 additions & 0 deletions packages/immutadot/src/core/curry.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,67 @@ describe('curry.curry', () => {
it('should discard extra args in last curried call', () => {
expect(curriedFill('path', 'value')('obj', 'extraArg')).toBe('fill(obj, path, value, 0, -1, undefined)')
})

it('should allow reusing partial calls', () => {
const partial1 = curriedFill('path')
const partial2 = partial1('value1')
const partial3 = partial1('value2')

expect(partial2('obj1')).toBe('fill(obj1, path, value1, 0, -1, undefined)')
expect(partial2('obj2')).toBe('fill(obj2, path, value1, 0, -1, undefined)')
expect(partial3('obj1')).toBe('fill(obj1, path, value2, 0, -1, undefined)')
expect(partial3('obj2')).toBe('fill(obj2, path, value2, 0, -1, undefined)')
})

it('should manage fixedArity', () => {
function printArgs(...args) {
return args.map((arg, i) => `arg${i} = ${arg}`).join(', ')
}

const printArgs2 = curry(printArgs, {
arity: 2,
fixedArity: true,
})
expect(printArgs2(1)(2)).toBe('arg0 = 2, arg1 = 1')

const printArgs3 = curry(printArgs, {
arity: 3,
fixedArity: true,
})
expect(printArgs3(1, 2)(3)).toBe('arg0 = 3, arg1 = 1, arg2 = 2')
expect(printArgs3(1)(2, 3)).toBe('arg0 = 3, arg1 = 1, arg2 = 2')
expect(printArgs3(1)(2)(3)).toBe('arg0 = 3, arg1 = 1, arg2 = 2')

const printArgs4 = curry(printArgs, {
arity: 4,
fixedArity: true,
})
expect(printArgs4(1, 2, 3)(4)).toBe('arg0 = 4, arg1 = 1, arg2 = 2, arg3 = 3')
expect(printArgs4(1, 2)(3, 4)).toBe('arg0 = 4, arg1 = 1, arg2 = 2, arg3 = 3')
expect(printArgs4(1, 2)(3)(4)).toBe('arg0 = 4, arg1 = 1, arg2 = 2, arg3 = 3')
expect(printArgs4(1)(2, 3, 4)).toBe('arg0 = 4, arg1 = 1, arg2 = 2, arg3 = 3')
expect(printArgs4(1)(2, 3)(4)).toBe('arg0 = 4, arg1 = 1, arg2 = 2, arg3 = 3')
expect(printArgs4(1)(2)(3, 4)).toBe('arg0 = 4, arg1 = 1, arg2 = 2, arg3 = 3')
expect(printArgs4(1)(2)(3)(4)).toBe('arg0 = 4, arg1 = 1, arg2 = 2, arg3 = 3')

const printArgs5 = curry(printArgs, {
arity: 5,
fixedArity: true,
})
expect(printArgs5(1, 2, 3, 4)(5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1, 2, 3)(4, 5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1, 2, 3)(4)(5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1, 2)(3, 4, 5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1, 2)(3, 4)(5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1, 2)(3)(4, 5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1, 2)(3)(4)(5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1)(2, 3, 4, 5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1)(2, 3, 4)(5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1)(2, 3)(4, 5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1)(2, 3)(4)(5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1)(2)(3, 4, 5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1)(2)(3, 4)(5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1)(2)(3)(4, 5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
expect(printArgs5(1)(2)(3)(4)(5)).toBe('arg0 = 5, arg1 = 1, arg2 = 2, arg3 = 3, arg4 = 4')
})
})