Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: export util functions for maps and improve documentation of scope #3243

Merged
merged 4 commits into from
Aug 1, 2024
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
11 changes: 7 additions & 4 deletions docs/expressions/customization.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,10 +114,13 @@ Where :

- `args` is an Array with nodes of the parsed arguments.
- `math` is the math namespace against which the expression was compiled.
- `scope` is a `Map` containing the variables defined in the scope passed
via `evaluate(scope)`. In case of using a custom defined function like
`f(x) = rawFunction(x) ^ 2`, the scope passed to `rawFunction` also contains
the current value of parameter `x`.
- `scope` is a [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map)
interface containing the variables defined in the scope
passed via `evaluate(scope)`. The passed scope is always a `Map` interface,
and normally a `PartitionedMap` is used to separate local function variables
like `x` in a custom defined function `f(x) = rawFunction(x) ^ 2` from the
scope variables. Note that a `PartitionedMap` can recursively link to another
`PartitionedMap`.

Raw functions must be imported in the `math` namespace, as they need to be
processed at compile time. They are not supported when passed via a scope
Expand Down
2 changes: 1 addition & 1 deletion docs/expressions/expression_trees.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ In this case, the expression `sqrt(2 + x)` is parsed as:
ConstantNode 2 x SymbolNode
```

Alternatively, this expression tree can be build by manually creating nodes:
Alternatively, this expression tree can be built by manually creating nodes:

```js
const node1 = new math.ConstantNode(2)
Expand Down
16 changes: 13 additions & 3 deletions docs/expressions/parsing.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,19 @@ math.evaluate([expr1, expr2, expr3, ...], scope)

Function `evaluate` accepts a single expression or an array with
expressions as the first argument and has an optional second argument
containing a scope with variables and functions. The scope can be a regular
JavaScript Object, or Map. The scope will be used to resolve symbols, and to write
assigned variables or function.
containing a `scope` with variables and functions. The scope can be a regular
JavaScript `Map` (recommended), a plain JavaScript `object`, or any custom
class that implements the `Map` interface with methods `get`, `set`, `keys`
and `has`. The scope will be used to resolve symbols, and to write assigned
variables and functions.

When an `Object` is used as scope, mathjs will internally wrap it in an
`ObjectWrappingMap` interface since the internal functions can only use a `Map`
interface. In case of custom defined functions like `f(x) = x^2`, the scope
will be wrapped in a `PartitionedMap`, which reads and writes the function
variables (like `x` in this example) from a temporary map, and reads and writes
other variables from the original scope. The original scope is never copied, it
is only wrapped around when needed.

The following code demonstrates how to evaluate expressions.

Expand Down
36 changes: 15 additions & 21 deletions examples/advanced/custom_scope_objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import { all, create } from '../../lib/esm/index.js'

const math = create(all)

// The expression evaluator accepts an optional scope object.
// This is the symbol table for variable defintions and function declations.
// The expression evaluator accepts an optional scope Map or object that can
// be used to keep additional variables and functions.

// Scope can be a bare object.
function withObjectScope () {
Expand All @@ -28,11 +28,11 @@ function withMapScope (scope, name) {
math.evaluate('area(length, width) = length * width * scalar', scope)
math.evaluate('A = area(x, y)', scope)

console.log(`Map-like scope (${name}):`, scope.localScope)
console.log(`Map-like scope (${name}):`, scope)
}

// This is a minimal set of functions to look like a Map.
class MapScope {
class CustomMap {
constructor () {
this.localScope = new Map()
}
Expand Down Expand Up @@ -61,7 +61,7 @@ class MapScope {
* used in mathjs.
*
*/
class AdvancedMapScope extends MapScope {
class AdvancedCustomMap extends CustomMap {
constructor (parent) {
super()
this.parentScope = parent
Expand Down Expand Up @@ -91,25 +91,19 @@ class AdvancedMapScope extends MapScope {
return this.localScope.clear()
}

/**
* Creates a child scope from this one. This is used in function calls.
*
* @returns a new Map scope that has access to the symbols in the parent, but
* cannot overwrite them.
*/
createSubScope () {
return new AdvancedMapScope(this)
}

toString () {
return this.localScope.toString()
}
}

// Use a plain JavaScript object
withObjectScope()
// Where safety is important, scope can also be a Map
withMapScope(new Map(), 'simple Map')
// Where flexibility is important, scope can duck type appear to be a Map.
withMapScope(new MapScope(), 'MapScope example')
// Extra methods allow even finer grain control.
withMapScope(new AdvancedMapScope(), 'AdvancedScope example')

// use a Map (recommended)
withMapScope(new Map(), 'Map example')

// Use a custom Map implementation
withMapScope(new CustomMap(), 'CustomMap example')

// Use a more advanced custom Map implementation
withMapScope(new AdvancedCustomMap(), 'AdvancedCustomMap example')
26 changes: 16 additions & 10 deletions src/core/create.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import typedFunction from 'typed-function'
import { deepFlatten, isLegacyFactory } from '../utils/object.js'
import * as emitter from './../utils/emitter.js'
import { importFactory } from './function/import.js'
import { configFactory } from './function/config.js'
import { ArgumentsError } from '../error/ArgumentsError.js'
import { DimensionError } from '../error/DimensionError.js'
import { IndexError } from '../error/IndexError.js'
import { factory, isFactory } from '../utils/factory.js'
import {
isAccessorNode,
isArray,
isArrayNode,
isAssignmentNode,
isBigInt,
isBigNumber,
isBlockNode,
isBoolean,
Expand All @@ -26,30 +26,33 @@ import {
isHelp,
isIndex,
isIndexNode,
isMap,
isMatrix,
isNode,
isNull,
isNumber,
isObject,
isObjectNode,
isObjectWrappingMap,
isOperatorNode,
isParenthesisNode,
isPartitionedMap,
isRange,
isRangeNode,
isRelationalNode,
isRegExp,
isRelationalNode,
isResultSet,
isSparseMatrix,
isString,
isSymbolNode,
isUndefined,
isUnit,
isBigInt
isUnit
} from '../utils/is.js'
import { ArgumentsError } from '../error/ArgumentsError.js'
import { DimensionError } from '../error/DimensionError.js'
import { IndexError } from '../error/IndexError.js'
import { deepFlatten, isLegacyFactory } from '../utils/object.js'
import * as emitter from './../utils/emitter.js'
import { DEFAULT_CONFIG } from './config.js'
import { configFactory } from './function/config.js'
import { importFactory } from './function/import.js'

/**
* Create a mathjs instance from given factory functions and optionally config
Expand Down Expand Up @@ -126,6 +129,9 @@ export function create (factories, config) {
isDate,
isRegExp,
isObject,
isMap,
isPartitionedMap,
isObjectWrappingMap,
isNull,
isUndefined,

Expand Down
11 changes: 6 additions & 5 deletions src/core/function/typed.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@
* @returns {function} The created typed-function.
*/

import typedFunction from 'typed-function'
import { factory } from '../../utils/factory.js'
import {
isAccessorNode,
isArray,
isArrayNode,
isAssignmentNode,
isBigInt,
isBigNumber,
isBlockNode,
isBoolean,
Expand All @@ -58,6 +61,7 @@ import {
isHelp,
isIndex,
isIndexNode,
isMap,
isMatrix,
isNode,
isNull,
Expand All @@ -68,19 +72,16 @@ import {
isParenthesisNode,
isRange,
isRangeNode,
isRelationalNode,
isRegExp,
isRelationalNode,
isResultSet,
isSparseMatrix,
isString,
isSymbolNode,
isUndefined,
isUnit, isBigInt
isUnit
} from '../../utils/is.js'
import typedFunction from 'typed-function'
import { digits } from '../../utils/number.js'
import { factory } from '../../utils/factory.js'
import { isMap } from '../../utils/map.js'

// returns a new instance of typed-function
let _createTyped = function () {
Expand Down
3 changes: 3 additions & 0 deletions src/entry/typeChecks.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ export {
isString,
isUndefined,
isObject,
isMap,
isPartitionedMap,
isObjectWrappingMap,
isObjectNode,
isOperatorNode,
isParenthesisNode,
Expand Down
2 changes: 1 addition & 1 deletion src/expression/node/FunctionNode.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export const createFunctionNode = /* #__PURE__ */ factory(name, dependencies, ({
* invoke a list with arguments on a node
* @param {./Node | string} fn
* Item resolving to a function on which to invoke
* the arguments, typically a SymboNode or AccessorNode
* the arguments, typically a SymbolNode or AccessorNode
* @param {./Node[]} args
*/
constructor (fn, args) {
Expand Down
34 changes: 34 additions & 0 deletions src/utils/is.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
// for security reasons, so these functions are not exposed in the expression
// parser.

import { ObjectWrappingMap } from './map.js'

export function isNumber (x) {
return typeof x === 'number'
}
Expand Down Expand Up @@ -125,6 +127,38 @@ export function isObject (x) {
!isFraction(x))
}

/**
* Returns `true` if the passed object appears to be a Map (i.e. duck typing).
*
* Methods looked for are `get`, `set`, `keys` and `has`.
*
* @param {Map | object} object
* @returns
*/
export function isMap (object) {
// We can use the fast instanceof, or a slower duck typing check.
// The duck typing method needs to cover enough methods to not be confused with DenseMatrix.
if (!object) {
return false
}
return object instanceof Map ||
object instanceof ObjectWrappingMap ||
(
typeof object.set === 'function' &&
typeof object.get === 'function' &&
typeof object.keys === 'function' &&
typeof object.has === 'function'
)
}

export function isPartitionedMap (object) {
return isMap(object) && isMap(object.a) && isMap(object.b)
}

export function isObjectWrappingMap (object) {
return isMap(object) && isObject(object.wrappedObject)
}

export function isNull (x) {
return x === null
}
Expand Down
28 changes: 2 additions & 26 deletions src/utils/map.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { setSafeProperty, hasSafeProperty, getSafeProperty } from './customs.js'
import { isObject } from './is.js'
import { getSafeProperty, hasSafeProperty, setSafeProperty } from './customs.js'
import { isMap, isObject } from './is.js'

/**
* A map facade on a bare object.
Expand Down Expand Up @@ -202,30 +202,6 @@ export function toObject (map) {
return object
}

/**
* Returns `true` if the passed object appears to be a Map (i.e. duck typing).
*
* Methods looked for are `get`, `set`, `keys` and `has`.
*
* @param {Map | object} object
* @returns
*/
export function isMap (object) {
// We can use the fast instanceof, or a slower duck typing check.
// The duck typing method needs to cover enough methods to not be confused with DenseMatrix.
if (!object) {
return false
}
return object instanceof Map ||
object instanceof ObjectWrappingMap ||
(
typeof object.set === 'function' &&
typeof object.get === 'function' &&
typeof object.keys === 'function' &&
typeof object.has === 'function'
)
}

/**
* Copies the contents of key-value pairs from each `objects` in to `map`.
*
Expand Down
3 changes: 3 additions & 0 deletions test/node-tests/doc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ const knownUndocumented = new Set([
'isDate',
'isRegExp',
'isObject',
'isMap',
'isPartitionedMap',
'isObjectWrappingMap',
'isNull',
'isUndefined',
'isAccessorNode',
Expand Down
3 changes: 3 additions & 0 deletions test/typescript-tests/testTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2293,6 +2293,9 @@ Factory Test
math.isDate,
math.isRegExp,
math.isObject,
math.isMap,
math.isPartitionedMap,
math.isObjectWrappingMap,
math.isNull,
math.isUndefined,
math.isAccessorNode,
Expand Down
Loading