diff --git a/src/expression/transform/filter.transform.js b/src/expression/transform/filter.transform.js index 378ab347d0..e8d3d86730 100644 --- a/src/expression/transform/filter.transform.js +++ b/src/expression/transform/filter.transform.js @@ -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'] @@ -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') - }) -} diff --git a/src/expression/transform/forEach.transform.js b/src/expression/transform/forEach.transform.js index 4ab5888553..08dd8a28e7 100644 --- a/src/expression/transform/forEach.transform.js +++ b/src/expression/transform/forEach.transform.js @@ -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' @@ -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 }) diff --git a/src/expression/transform/map.transform.js b/src/expression/transform/map.transform.js index 8394887475..9dd8638aa0 100644 --- a/src/expression/transform/map.transform.js +++ b/src/expression/transform/map.transform.js @@ -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'] @@ -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) { @@ -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) { @@ -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) @@ -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) -} diff --git a/src/expression/transform/utils/transformCallback.js b/src/expression/transform/utils/transformCallback.js new file mode 100644 index 0000000000..ba5f466080 --- /dev/null +++ b/src/expression/transform/utils/transformCallback.js @@ -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) +} diff --git a/src/function/matrix/filter.js b/src/function/matrix/filter.js index c363a88259..1b8c832647 100644 --- a/src/function/matrix/filter.js +++ b/src/function/matrix/filter.js @@ -1,4 +1,4 @@ -import { applyCallback } from '../../utils/applyCallback.js' +import { optimizeCallback } from '../../utils/optimizeCallback.js' import { filter, filterRegExp } from '../../utils/array.js' import { factory } from '../../utils/factory.js' @@ -58,8 +58,9 @@ export const createFilter = /* #__PURE__ */ factory(name, dependencies, ({ typed * @private */ function _filterCallback (x, callback) { + const fastCallback = optimizeCallback(callback, x, 'filter') return filter(x, function (value, index, array) { // invoke the callback function with the right number of arguments - return applyCallback(callback, value, [index], array, 'filter') + return fastCallback(value, [index], array) }) } diff --git a/src/function/matrix/forEach.js b/src/function/matrix/forEach.js index d31a6f1bf4..3dfa266278 100644 --- a/src/function/matrix/forEach.js +++ b/src/function/matrix/forEach.js @@ -1,6 +1,6 @@ -import { applyCallback } from '../../utils/applyCallback.js' -import { forEach as forEachArray } from '../../utils/array.js' +import { optimizeCallback } from '../../utils/optimizeCallback.js' import { factory } from '../../utils/factory.js' +import { recurse } from '../../utils/array.js' const name = 'forEach' const dependencies = ['typed'] @@ -45,16 +45,5 @@ export const createForEach = /* #__PURE__ */ factory(name, dependencies, ({ type * @private */ function _forEach (array, callback) { - const recurse = function (value, index) { - if (Array.isArray(value)) { - forEachArray(value, function (child, i) { - // we create a copy of the index array and append the new index value - recurse(child, index.concat(i)) - }) - } else { - // invoke the callback function with the right number of arguments - return applyCallback(callback, value, index, array, 'forEach') - } - } - recurse(array, []) + recurse(array, [], array, optimizeCallback(callback, array, name)) } diff --git a/src/function/matrix/map.js b/src/function/matrix/map.js index 0f46c32f3d..d8ff940431 100644 --- a/src/function/matrix/map.js +++ b/src/function/matrix/map.js @@ -1,5 +1,5 @@ -import { applyCallback } from '../../utils/applyCallback.js' -import { arraySize, broadcastSizes, broadcastTo, get } from '../../utils/array.js' +import { optimizeCallback } from '../../utils/optimizeCallback.js' +import { arraySize, broadcastSizes, broadcastTo, get, recurse } from '../../utils/array.js' import { factory } from '../../utils/factory.js' const name = 'map' @@ -143,36 +143,14 @@ export const createMap = /* #__PURE__ */ factory(name, dependencies, ({ typed }) return 0 } } -}) - -/** + /** * Map for a multi dimensional array * @param {Array} array * @param {Function} callback * @return {Array} * @private */ -function _mapArray (array, callback) { - return _recurse(array, [], array, callback) -} - -/** - * Recursive function to map a multi-dimensional array. - * - * @param {*} value - The current value being processed in the array. - * @param {Array} index - The index of the current value being processed in the array. - * @param {Array} array - The array being processed. - * @param {Function} callback - Function that produces the element of the new Array, taking three arguments: the value of the element, the index of the element, and the Array being processed. - * @returns {*} The new array with each element being the result of the callback function. - */ -function _recurse (value, index, array, callback) { - if (Array.isArray(value)) { - return value.map(function (child, i) { - // we create a copy of the index array and append the new index value - return _recurse(child, index.concat(i), array, callback) - }) - } else { - // invoke the callback function with the right number of arguments - return applyCallback(callback, value, index, array, 'map') + function _mapArray (array, callback) { + return recurse(array, [], array, optimizeCallback(callback, array, name)) } -} +}) diff --git a/src/type/matrix/DenseMatrix.js b/src/type/matrix/DenseMatrix.js index 13e01c0994..a05a593042 100644 --- a/src/type/matrix/DenseMatrix.js +++ b/src/type/matrix/DenseMatrix.js @@ -1,11 +1,11 @@ import { isArray, isBigNumber, isCollection, isIndex, isMatrix, isNumber, isString, typeOf } from '../../utils/is.js' -import { arraySize, getArrayDataType, processSizesWildcard, reshape, resize, unsqueeze, validate, validateIndex, broadcastTo, get } from '../../utils/array.js' +import { arraySize, getArrayDataType, processSizesWildcard, reshape, resize, unsqueeze, validate, validateIndex, broadcastTo, get, recurse } from '../../utils/array.js' import { format } from '../../utils/string.js' import { isInteger } from '../../utils/number.js' import { clone, deepStrictEqual } from '../../utils/object.js' import { DimensionError } from '../../error/DimensionError.js' import { factory } from '../../utils/factory.js' -import { applyCallback } from '../../utils/applyCallback.js' +import { optimizeCallback } from '../../utils/optimizeCallback.js' const name = 'DenseMatrix' const dependencies = [ @@ -537,21 +537,12 @@ export const createDenseMatrixClass = /* #__PURE__ */ factory(name, dependencies DenseMatrix.prototype.map = function (callback) { // matrix instance const me = this - const recurse = function (value, index) { - if (isArray(value)) { - return value.map(function (child, i) { - return recurse(child, index.concat(i)) - }) - } else { - // invoke the callback function with the right number of arguments - return applyCallback(callback, value, index, me, 'map') - } - } + const fastCallback = optimizeCallback(callback, me._data, 'map') // determine the new datatype when the original matrix has datatype defined // TODO: should be done in matrix constructor instead - const data = recurse(this._data, []) - const datatype = this._datatype !== undefined + const data = recurse(me._data, [], me, fastCallback) + const datatype = me._datatype !== undefined ? getArrayDataType(data, typeOf) : undefined return new DenseMatrix(data, datatype) @@ -567,16 +558,8 @@ export const createDenseMatrixClass = /* #__PURE__ */ factory(name, dependencies DenseMatrix.prototype.forEach = function (callback) { // matrix instance const me = this - const recurse = function (value, index) { - if (isArray(value)) { - value.forEach(function (child, i) { - recurse(child, index.concat(i)) - }) - } else { - callback(value, index, me) - } - } - recurse(this._data, []) + const fastCallback = optimizeCallback(callback, me._data, 'forEach') + recurse(this._data, [], me, fastCallback) } /** diff --git a/src/type/matrix/SparseMatrix.js b/src/type/matrix/SparseMatrix.js index 4f573827d2..355cfb297d 100644 --- a/src/type/matrix/SparseMatrix.js +++ b/src/type/matrix/SparseMatrix.js @@ -5,7 +5,7 @@ import { clone, deepStrictEqual } from '../../utils/object.js' import { arraySize, getArrayDataType, processSizesWildcard, unsqueeze, validateIndex } from '../../utils/array.js' import { factory } from '../../utils/factory.js' import { DimensionError } from '../../error/DimensionError.js' -import { applyCallback } from '../../utils/applyCallback.js' +import { optimizeCallback } from '../../utils/optimizeCallback.js' const name = 'SparseMatrix' const dependencies = [ @@ -853,10 +853,11 @@ export const createSparseMatrixClass = /* #__PURE__ */ factory(name, dependencie // rows and columns const rows = this._size[0] const columns = this._size[1] + const fastCallback = optimizeCallback(callback, me, 'map') // invoke callback const invoke = function (v, i, j) { // invoke callback - return applyCallback(callback, v, [i, j], me, 'map') + return fastCallback(v, [i, j], me) } // invoke _map return _map(this, 0, rows - 1, 0, columns - 1, invoke, skipZeros) @@ -961,6 +962,7 @@ export const createSparseMatrixClass = /* #__PURE__ */ factory(name, dependencie // rows and columns const rows = this._size[0] const columns = this._size[1] + const fastCallback = optimizeCallback(callback, me, 'forEach') // loop columns for (let j = 0; j < columns; j++) { // k0 <= k < k1 where k0 = _ptr[j] && k1 = _ptr[j+1] @@ -974,7 +976,7 @@ export const createSparseMatrixClass = /* #__PURE__ */ factory(name, dependencie const i = this._index[k] // value @ k - callback(this._values[k], [i, j], me) + fastCallback(this._values[k], [i, j], me) } } else { // create a cache holding all defined values @@ -988,7 +990,7 @@ export const createSparseMatrixClass = /* #__PURE__ */ factory(name, dependencie // and either read the value or zero for (let i = 0; i < rows; i++) { const value = (i in values) ? values[i] : 0 - callback(value, [i, j], me) + fastCallback(value, [i, j], me) } } } @@ -1380,6 +1382,7 @@ export const createSparseMatrixClass = /* #__PURE__ */ factory(name, dependencie // indeces for column j const k0 = ptr[j] const k1 = ptr[j + 1] + // loop for (let k = k0; k < k1; k++) { // invoke callback diff --git a/src/utils/applyCallback.js b/src/utils/applyCallback.js deleted file mode 100644 index 0135efe1c9..0000000000 --- a/src/utils/applyCallback.js +++ /dev/null @@ -1,67 +0,0 @@ -import typed from 'typed-function' -import { typeOf as _typeOf } from './is.js' - -/** - * Invoke a callback for functions like map and filter with a matching number of arguments - * @param {function} callback - * @param {any} value - * @param {number | number[]} index - * @param {Array} array - * @param {string} mappingFnName The name of the function that is invoking these callbacks, for example "map" or "filter" - * @returns {*} - */ -export function applyCallback (callback, value, index, array, mappingFnName) { - if (typed.isTypedFunction(callback)) { - // invoke the typed callback function with the matching number of arguments only - - const args3 = [value, index, array] - const signature3 = typed.resolve(callback, args3) - if (signature3) { - return tryWithArgs(signature3.implementation, args3) - } - - const args2 = [value, index] - const signature2 = typed.resolve(callback, args2) - if (signature2) { - return tryWithArgs(signature2.implementation, args2) - } - - const args1 = [value] - const signature1 = typed.resolve(callback, args1) - if (signature1) { - return tryWithArgs(signature1.implementation, args1) - } - - // fallback (will throw an exception) - return tryWithArgs(callback, args3) - } else { - // A regular JavaScript function - return callback(value, index, array) - } - - /** - * @param {function} signature The selected signature of the typed-function - * @param {Array} args List with arguments to apply to the selected signature - * @returns {*} Returns the return value of the invoked signature - * @throws {TypeError} Throws an error when no matching signature was found - */ - function tryWithArgs (signature, args) { - try { - return signature.apply(signature, args) - } catch (err) { - // Enrich the error message so the user understands that it took place inside the callback function - if (err instanceof TypeError && err.data?.category === 'wrongType') { - const argsDesc = [] - argsDesc.push(`value: ${_typeOf(value)}`) - if (args.length >= 2) { argsDesc.push(`index: ${_typeOf(index)}`) } - if (args.length >= 3) { argsDesc.push(`array: ${_typeOf(array)}`) } - - throw new TypeError(`Function ${mappingFnName} cannot apply callback arguments ` + - `${callback.name}(${argsDesc.join(', ')}) at index ${JSON.stringify(index)}`) - } else { - throw new TypeError(`Function ${mappingFnName} cannot apply callback arguments ` + - `to function ${callback.name}: ${err.message}`) - } - } - } -} diff --git a/src/utils/array.js b/src/utils/array.js index 1ddc5a7628..4017983361 100644 --- a/src/utils/array.js +++ b/src/utils/array.js @@ -825,6 +825,27 @@ export function get (array, index) { return index.reduce((acc, curr) => acc[curr], array) } +/** + * Recursive function to map a multi-dimensional array. + * + * @param {*} value - The current value being processed in the array. + * @param {Array} index - The index of the current value being processed in the array. + * @param {Array} array - The array being processed. + * @param {Function} callback - Function that produces the element of the new Array, taking three arguments: the value of the element, the index of the element, and the Array being processed. + * @returns {*} The new array with each element being the result of the callback function. + */ +export function recurse (value, index, array, callback) { + if (Array.isArray(value)) { + return value.map(function (child, i) { + // we create a copy of the index array and append the new index value + return recurse(child, index.concat(i), array, callback) + }) + } else { + // invoke the callback function with the right number of arguments + return callback(value, index, array) + } +} + /** * Deep clones a multidimensional array * @param {Array} array diff --git a/src/utils/optimizeCallback.js b/src/utils/optimizeCallback.js new file mode 100644 index 0000000000..4f0166ee30 --- /dev/null +++ b/src/utils/optimizeCallback.js @@ -0,0 +1,77 @@ +import typed from 'typed-function' +import { get, arraySize } from './array.js' +import { typeOf as _typeOf } from './is.js' + +/** + * Simplifies a callback function by reducing its complexity and potentially improving its performance. + * + * @param {Function} callback The original callback function to simplify. + * @param {Array|Matrix} array The array that will be used with the callback function. + * @param {string} name The name of the function that is using the callback. + * @returns {Function} Returns a simplified version of the callback function. + */ +export function optimizeCallback (callback, array, name) { + if (typed.isTypedFunction(callback)) { + const firstIndex = (array.isMatrix ? array.size() : arraySize(array)).map(() => 0) + const firstValue = array.isMatrix ? array.get(firstIndex) : get(array, firstIndex) + const hasSingleSignature = Object.keys(callback.signatures).length === 1 + const numberOfArguments = _findNumberOfArguments(callback, firstValue, firstIndex, array) + const fastCallback = hasSingleSignature ? Object.values(callback.signatures)[0] : callback + if (numberOfArguments >= 1 && numberOfArguments <= 3) { + return (...args) => _tryFunctionWithArgs(fastCallback, args.slice(0, numberOfArguments), name, callback.name) + } + return (...args) => _tryFunctionWithArgs(fastCallback, args, name, callback.name) + } + return callback +} + +function _findNumberOfArguments (callback, value, index, array) { + const testArgs = [value, index, array] + for (let i = 3; i > 0; i--) { + const args = testArgs.slice(0, i) + if (typed.resolve(callback, args) !== null) { + return i + } + } +} + +/** + * @param {function} func The selected function taken from one of the signatures of the callback function + * @param {Array} args List with arguments to apply to the selected signature + * @param {string} mappingFnName the name of the function that is using the callback + * @param {string} callbackName the name of the callback function + * @returns {*} Returns the return value of the invoked signature + * @throws {TypeError} Throws an error when no matching signature was found + */ +function _tryFunctionWithArgs (func, args, mappingFnName, callbackName) { + try { + return func(...args) + } catch (err) { + _createCallbackError(err, args, mappingFnName, callbackName) + } +} + +/** + * Creates and throws a detailed TypeError when a callback function fails. + * + * @param {Error} err The original error thrown by the callback function. + * @param {Array} args The arguments that were passed to the callback function. + * @param {string} mappingFnName The name of the function that is using the callback. + * @param {string} callbackName The name of the callback function. + * @throws {TypeError} Throws a detailed TypeError with enriched error message. + */ +function _createCallbackError (err, args, mappingFnName, callbackName) { + // Enrich the error message so the user understands that it took place inside the callback function + if (err instanceof TypeError && err.data?.category === 'wrongType') { + const argsDesc = [] + argsDesc.push(`value: ${_typeOf(args[0])}`) + if (args.length >= 2) { argsDesc.push(`index: ${_typeOf(args[1])}`) } + if (args.length >= 3) { argsDesc.push(`array: ${_typeOf(args[2])}`) } + + throw new TypeError(`Function ${mappingFnName} cannot apply callback arguments ` + + `${callbackName}(${argsDesc.join(', ')}) at index ${JSON.stringify(args[1])}`) + } else { + throw new TypeError(`Function ${mappingFnName} cannot apply callback arguments ` + + `to function ${callbackName}: ${err.message}`) + } +} diff --git a/test/benchmark/forEach.js b/test/benchmark/forEach.js new file mode 100644 index 0000000000..7f3f3da510 --- /dev/null +++ b/test/benchmark/forEach.js @@ -0,0 +1,55 @@ +import Benchmark from 'benchmark' +import padRight from 'pad-right' +import { ones, abs, DenseMatrix, map, forEach, random, round } from '../../lib/esm/index.js' + +const genericMatrix = map(ones(10, 10, 'dense'), _ => round(random(-5, 5), 2)) +const numberMatrix = new DenseMatrix(genericMatrix, 'number') +const array = genericMatrix.toArray() + +// console.log('data', array) +// console.log('abs(data)', abs(array))npm run + +new Benchmark.Suite() + .add(pad('abs(genericMatrix)'), () => { + abs(genericMatrix) + }) + .add(pad('abs(array)'), () => { + abs(array) + }) + .add(pad('abs(numberMatrix)'), () => { + abs(numberMatrix) + }) + .add(pad('genericMatrix.forEach(abs)'), () => { + genericMatrix.forEach(abs) + }) + .add(pad('numberMatrix.forEach(abs)'), () => { + numberMatrix.forEach(abs) + }) + .add(pad('forEach(genericMatrix, abs)'), () => { + forEach(genericMatrix, abs) + }) + .add(pad('forEach(numberMatrix, abs)'), () => { + forEach(numberMatrix, abs) + }) + .add(pad('forEach(array, abs)'), () => { + forEach(array, abs) + }) + .add(pad('forEach(array, abs.signatures.number)'), () => { + forEach(array, abs.signatures.number) + }) + .add(pad('genericMatrix.forEach(abs.signatures.number)'), () => { + genericMatrix.forEach(abs.signatures.number) + }) + .add(pad('numberMatrix.forEach(abs.signatures.number)'), () => { + numberMatrix.forEach(abs.signatures.number) + }) + .on('cycle', function (event) { + console.log(String(event.target)) + }) + .on('complete', function () { + }) + .run() + +function pad (text) { + return padRight(text, 45, ' ') +} diff --git a/test/benchmark/index.js b/test/benchmark/index.js index 2f33e29d92..7ada9f253d 100644 --- a/test/benchmark/index.js +++ b/test/benchmark/index.js @@ -7,3 +7,4 @@ import './prime.js' import './load.js' import './scope_variables.js' import './map.js' +import './forEach.js' diff --git a/test/benchmark/map.js b/test/benchmark/map.js index f8b33482bb..05b0ea3986 100644 --- a/test/benchmark/map.js +++ b/test/benchmark/map.js @@ -35,7 +35,7 @@ new Benchmark.Suite() map(array, abs) }) .add(pad('map(array, abs.signatures.number)'), () => { - map(array, abs) + map(array, abs.signatures.number) }) .add(pad('genericMatrix.map(abs.signatures.number)'), () => { genericMatrix.map(abs.signatures.number)