Skip to content

Commit

Permalink
chore: improve performance of functions map, filter and forEach (
Browse files Browse the repository at this point in the history
…#3256)

* Implement reduceCallback

* Add jsdocs

* implement simplifyCallback in other functions

* Moved recurse to array.js

* Format

* Separate transform callback

* forEach transform

* Renamed applyCallback to simplifyCallback

* Simplified index transform

* renamed to reducedCallback and simplifiedCallback to simpleCallback

* chore: fix linting issue

* Added forEach benchmark

* renamed simplifyCallback to optimizeCallback

---------

Co-authored-by: Jos de Jong <[email protected]>
  • Loading branch information
dvd101x and josdejong authored Sep 11, 2024
1 parent 0f87a7b commit 367c0d3
Show file tree
Hide file tree
Showing 15 changed files with 330 additions and 300 deletions.
67 changes: 26 additions & 41 deletions src/expression/transform/filter.transform.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { applyCallback } from '../../utils/applyCallback.js'
import { filter, filterRegExp } from '../../utils/array.js'
import { createFilter } from '../../function/matrix/filter.js'
import { factory } from '../../utils/factory.js'
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { compileInlineExpression } from './utils/compileInlineExpression.js'
import { createTransformCallback } from './utils/transformCallback.js'

const name = 'filter'
const dependencies = ['typed']
Expand All @@ -16,57 +16,42 @@ export const createFilterTransform = /* #__PURE__ */ factory(name, dependencies,
* so you can do something like 'filter([3, -2, 5], x > 0)'.
*/
function filterTransform (args, math, scope) {
let x, callback
const filter = createFilter({ typed })
const transformCallback = createTransformCallback({ typed })

if (args[0]) {
x = args[0].compile().evaluate(scope)
if (args.length === 0) {
return filter()
}
let x = args[0]

if (args[1]) {
if (isSymbolNode(args[1]) || isFunctionAssignmentNode(args[1])) {
if (args.length === 1) {
return filter(x)
}

const N = args.length - 1
let callback = args[N]

if (x) {
x = _compileAndEvaluate(x, scope)
}

if (callback) {
if (isSymbolNode(callback) || isFunctionAssignmentNode(callback)) {
// a function pointer, like filter([3, -2, 5], myTestFunction)
callback = args[1].compile().evaluate(scope)
callback = _compileAndEvaluate(callback, scope)
} else {
// an expression like filter([3, -2, 5], x > 0)
callback = compileInlineExpression(args[1], math, scope)
callback = compileInlineExpression(callback, math, scope)
}
}

return filter(x, callback)
return filter(x, transformCallback(callback, N))
}
filterTransform.rawArgs = true

// one based version of function filter
const filter = typed('filter', {
'Array, function': _filter,

'Matrix, function': function (x, test) {
return x.create(_filter(x.toArray(), test), x.datatype())
},

'Array, RegExp': filterRegExp,

'Matrix, RegExp': function (x, test) {
return x.create(filterRegExp(x.toArray(), test), x.datatype())
}
})
function _compileAndEvaluate (arg, scope) {
return arg.compile().evaluate(scope)
}

return filterTransform
}, { isTransformFunction: true })

/**
* Filter values in a callback given a callback function
*
* !!! Passes a one-based index !!!
*
* @param {Array} x
* @param {Function} callback
* @return {Array} Returns the filtered array
* @private
*/
function _filter (x, callback) {
return filter(x, function (value, index, array) {
// invoke the callback function with the right number of arguments
return applyCallback(callback, value, [index + 1], array, 'filter')
})
}
57 changes: 27 additions & 30 deletions src/expression/transform/forEach.transform.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { applyCallback } from '../../utils/applyCallback.js'
import { forEach } from '../../utils/array.js'
import { createForEach } from '../../function/matrix/forEach.js'
import { createTransformCallback } from './utils/transformCallback.js'
import { factory } from '../../utils/factory.js'
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { compileInlineExpression } from './utils/compileInlineExpression.js'
Expand All @@ -14,44 +14,41 @@ export const createForEachTransform = /* #__PURE__ */ factory(name, dependencies
*
* This transform creates a one-based index instead of a zero-based index
*/
const forEach = createForEach({ typed })
const transformCallback = createTransformCallback({ typed })
function forEachTransform (args, math, scope) {
let x, callback
if (args.length === 0) {
return forEach()
}
let x = args[0]

if (args.length === 1) {
return forEach(x)
}

const N = args.length - 1
let callback = args[N]

if (args[0]) {
x = args[0].compile().evaluate(scope)
if (x) {
x = _compileAndEvaluate(x, scope)
}

if (args[1]) {
if (isSymbolNode(args[1]) || isFunctionAssignmentNode(args[1])) {
// a function pointer, like forEach([3, -2, 5], myTestFunction)
callback = args[1].compile().evaluate(scope)
if (callback) {
if (isSymbolNode(callback) || isFunctionAssignmentNode(callback)) {
// a function pointer, like filter([3, -2, 5], myTestFunction)
callback = _compileAndEvaluate(callback, scope)
} else {
// an expression like forEach([3, -2, 5], x > 0 ? callback1(x) : callback2(x) )
callback = compileInlineExpression(args[1], math, scope)
// an expression like filter([3, -2, 5], x > 0)
callback = compileInlineExpression(callback, math, scope)
}
}

return _forEach(x, callback)
return forEach(x, transformCallback(callback, N))
}
forEachTransform.rawArgs = true

// one-based version of forEach
const _forEach = typed('forEach', {
'Array | Matrix, function': function (array, callback) {
const recurse = function (value, index) {
if (Array.isArray(value)) {
forEach(value, function (child, i) {
// we create a copy of the index array and append the new index value
recurse(child, index.concat(i + 1)) // one based index, hence i+1
})
} else {
// invoke the callback function with the right number of arguments
return applyCallback(callback, value, index, array, 'forEach')
}
}
recurse(array.valueOf(), []) // pass Array
}
})

function _compileAndEvaluate (arg, scope) {
return arg.compile().evaluate(scope)
}
return forEachTransform
}, { isTransformFunction: true })
94 changes: 5 additions & 89 deletions src/expression/transform/map.transform.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { factory } from '../../utils/factory.js'
import { isFunctionAssignmentNode, isSymbolNode } from '../../utils/is.js'
import { createMap } from '../../function/matrix/map.js'
import { compileInlineExpression } from './utils/compileInlineExpression.js'
import { createTransformCallback } from './utils/transformCallback.js'

const name = 'map'
const dependencies = ['typed']
Expand All @@ -14,6 +15,7 @@ export const createMapTransform = /* #__PURE__ */ factory(name, dependencies, ({
* This transform creates a one-based index instead of a zero-based index
*/
const map = createMap({ typed })
const transformCallback = createTransformCallback({ typed })

function mapTransform (args, math, scope) {
if (args.length === 0) {
Expand All @@ -24,9 +26,8 @@ export const createMapTransform = /* #__PURE__ */ factory(name, dependencies, ({
return map(args[0])
}
const N = args.length - 1
let X, callback
callback = args[N]
X = args.slice(0, N)
let X = args.slice(0, N)
let callback = args[N]
X = X.map(arg => _compileAndEvaluate(arg, scope))

if (callback) {
Expand All @@ -38,7 +39,7 @@ export const createMapTransform = /* #__PURE__ */ factory(name, dependencies, ({
callback = compileInlineExpression(callback, math, scope)
}
}
return map(...X, _transformCallback(callback, N))
return map(...X, transformCallback(callback, N))

function _compileAndEvaluate (arg, scope) {
return arg.compile().evaluate(scope)
Expand All @@ -47,89 +48,4 @@ export const createMapTransform = /* #__PURE__ */ factory(name, dependencies, ({
mapTransform.rawArgs = true

return mapTransform

/**
* Transforms the given callback function based on its type and number of arrays.
*
* @param {Function} callback - The callback function to transform.
* @param {number} numberOfArrays - The number of arrays to pass to the callback function.
* @returns {*} - The transformed callback function.
*/
function _transformCallback (callback, numberOfArrays) {
if (typed.isTypedFunction(callback)) {
return _transformTypedCallbackFunction(callback, numberOfArrays)
} else {
return _transformCallbackFunction(callback, callback.length, numberOfArrays)
}
}

/**
* Transforms the given typed callback function based on the number of arrays.
*
* @param {Function} typedFunction - The typed callback function to transform.
* @param {number} numberOfArrays - The number of arrays to pass to the callback function.
* @returns {*} - The transformed typed callback function.
*/
function _transformTypedCallbackFunction (typedFunction, numberOfArrays) {
const signatures = Object.fromEntries(
Object.entries(typedFunction.signatures)
.map(([signature, callbackFunction]) => {
const numberOfCallbackInputs = signature.split(',').length
if (typed.isTypedFunction(callbackFunction)) {
return [signature, _transformTypedCallbackFunction(callbackFunction, numberOfArrays)]
} else {
return [signature, _transformCallbackFunction(callbackFunction, numberOfCallbackInputs, numberOfArrays)]
}
})
)

if (typeof typedFunction.name === 'string') {
return typed(typedFunction.name, signatures)
} else {
return typed(signatures)
}
}
}, { isTransformFunction: true })

/**
* Transforms the callback function based on the number of callback inputs and arrays.
* There are three cases:
* 1. The callback function has N arguments.
* 2. The callback function has N+1 arguments.
* 3. The callback function has 2N+1 arguments.
*
* @param {Function} callbackFunction - The callback function to transform.
* @param {number} numberOfCallbackInputs - The number of callback inputs.
* @param {number} numberOfArrays - The number of arrays.
* @returns {Function} The transformed callback function.
*/
function _transformCallbackFunction (callbackFunction, numberOfCallbackInputs, numberOfArrays) {
if (numberOfCallbackInputs === numberOfArrays) {
return callbackFunction
} else if (numberOfCallbackInputs === numberOfArrays + 1) {
return function (...args) {
const vals = args.slice(0, numberOfArrays)
const idx = _transformDims(args[numberOfArrays])
return callbackFunction(...vals, idx)
}
} else if (numberOfCallbackInputs > numberOfArrays + 1) {
return function (...args) {
const vals = args.slice(0, numberOfArrays)
const idx = _transformDims(args[numberOfArrays])
const rest = args.slice(numberOfArrays + 1)
return callbackFunction(...vals, idx, ...rest)
}
} else {
return callbackFunction
}
}

/**
* Transforms the dimensions by adding 1 to each dimension.
*
* @param {Array} dims - The dimensions to transform.
* @returns {Array} The transformed dimensions.
*/
function _transformDims (dims) {
return dims.map(dim => dim.isBigNumber ? dim.plus(1) : dim + 1)
}
91 changes: 91 additions & 0 deletions src/expression/transform/utils/transformCallback.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { factory } from '../../../utils/factory.js'

const name = 'transformCallback'
const dependencies = ['typed']

export const createTransformCallback = /* #__PURE__ */ factory(name, dependencies, ({ typed }) => {
/**
* Transforms the given callback function based on its type and number of arrays.
*
* @param {Function} callback - The callback function to transform.
* @param {number} numberOfArrays - The number of arrays to pass to the callback function.
* @returns {*} - The transformed callback function.
*/
return function (callback, numberOfArrays) {
if (typed.isTypedFunction(callback)) {
return _transformTypedCallbackFunction(callback, numberOfArrays)
} else {
return _transformCallbackFunction(callback, callback.length, numberOfArrays)
}
}

/**
* Transforms the given typed callback function based on the number of arrays.
*
* @param {Function} typedFunction - The typed callback function to transform.
* @param {number} numberOfArrays - The number of arrays to pass to the callback function.
* @returns {*} - The transformed callback function.
*/
function _transformTypedCallbackFunction (typedFunction, numberOfArrays) {
const signatures = Object.fromEntries(
Object.entries(typedFunction.signatures)
.map(([signature, callbackFunction]) => {
const numberOfCallbackInputs = signature.split(',').length
if (typed.isTypedFunction(callbackFunction)) {
return [signature, _transformTypedCallbackFunction(callbackFunction, numberOfArrays)]
} else {
return [signature, _transformCallbackFunction(callbackFunction, numberOfCallbackInputs, numberOfArrays)]
}
})
)

if (typeof typedFunction.name === 'string') {
return typed(typedFunction.name, signatures)
} else {
return typed(signatures)
}
}
})

/**
* Transforms the callback function based on the number of callback inputs and arrays.
* There are three cases:
* 1. The callback function has N arguments.
* 2. The callback function has N+1 arguments.
* 3. The callback function has 2N+1 arguments.
*
* @param {Function} callbackFunction - The callback function to transform.
* @param {number} numberOfCallbackInputs - The number of callback inputs.
* @param {number} numberOfArrays - The number of arrays.
* @returns {Function} The transformed callback function.
*/
function _transformCallbackFunction (callbackFunction, numberOfCallbackInputs, numberOfArrays) {
if (numberOfCallbackInputs === numberOfArrays) {
return callbackFunction
} else if (numberOfCallbackInputs === numberOfArrays + 1) {
return function (...args) {
const vals = args.slice(0, numberOfArrays)
const idx = _transformDims(args[numberOfArrays])
return callbackFunction(...vals, idx)
}
} else if (numberOfCallbackInputs > numberOfArrays + 1) {
return function (...args) {
const vals = args.slice(0, numberOfArrays)
const idx = _transformDims(args[numberOfArrays])
const rest = args.slice(numberOfArrays + 1)
return callbackFunction(...vals, idx, ...rest)
}
} else {
return callbackFunction
}
}

/**
* Transforms the dimensions by adding 1 to each dimension.
*
* @param {Array} dims - The dimensions to transform.
* @returns {Array} The transformed dimensions.
*/
function _transformDims (dims) {
return dims.map(dim => dim + 1)
}
Loading

0 comments on commit 367c0d3

Please sign in to comment.