Skip to content

Commit

Permalink
feat: export util functions for maps and improve documentation of `sc…
Browse files Browse the repository at this point in the history
…ope` (#3243)

* feat: export util functions `isMap`, `isPartitionedMap`, and `isObjectWrappingMap` and improve the documentation of `scope` (see #3150)

* chore: fix broken unit tests

* docs: refine the explanation about scopes
  • Loading branch information
josdejong authored Aug 1, 2024
1 parent 61c5d07 commit a1eec93
Show file tree
Hide file tree
Showing 16 changed files with 185 additions and 75 deletions.
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

0 comments on commit a1eec93

Please sign in to comment.