8000 usage errors · kennytilton/matrix Wiki · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

usage errors

Kenneth Tilton edited this page Apr 6, 2023 · 17 revisions

Usage errors

Matrix internals will trap several kinds of developer mistakes in using the API. When this happens, an exception is thrown which offers a lot of information about the Matrix elements involved. Here we expand on that information with a deeper discussion of the underlying issues.

Many developers prefer just diving headlong into a new technology to wading first through documentation, guided only by examples. [Raises hand. ed.] This section is for us. So dive in, hit an exception, and then learn more here.

First, a note about all these explications. Each will include something like "a property of this instance", emphasis added. Why the emphasis? Only because newcomers to Matrix may not have fully grasped that the same property of different instances can have different formulas, or no formula at all, and can be constants or so-called input variables.

Aside: this flexibility is how Matrix makes OO work: in Matrix, OO simply does not say as much, so we do not end up fighting OO. A Matrix property such as enabled? still has a a clear expression (can the user use the widget?), but its derivation can be tailored to different instances.

ie, each of these might appear in the same code, with md as an alias for t.matrix.api:

(md/make :enabled? true)

...or:

(md/make :widget :enabled? (cI false))

...or

(md/make :enabled? (cF (nil? (mget me :errors))) 

...or

(md/make :enabled? (cF (some-other-test me))

So when sorting out a problem, when an error does not make sense, we must make sure we are looking at the right property of the right instance.

And now the errors.

MXAPI_ILLEGAL_GET_NOSUCHSLOT / MXAPI_ILLEGAL_MUTATE_NOSUCHSLOT

In brief: no slot of the given name exists in the given instance.

How the exception text is generated:

MXAPI_ILLEGAL_GET_NOSUCHSLOT> mget was attempted on non-existent slot \"" slot "\".
...> FYI: known slots are" (keys @me))

...or:

MXAPI_ILLEGAL_MUTATE_NOSUCHSLOT> mswap!/mset!/md-reset! was attempted of non-existent slot \"" slot "\".
...> FYI: known slots are" (keys @me))

Discussion

This probably arises just from a developer typo in the slot name, but possibly is a case of a slot offered in prototype fashion to some instance, but not this one. Or perhaps the developer thinks the Matrix prototype model allows assignment to new properties after the call to make; that is not the case.

Deeper discussion: Clojure itself famously does not mind if we seek the binding of keyword not contained by a map, but Matrix objects are long-lived ones that rely on object identity, hence support mutation but with strict controls, and with most properties defined as functions of other Matrix properties. ie, An instance's properties may be mutated, but instances will not gain or lose properties during their lifetimes. Because of all that, these "no such slot" errors become possible and helpful. We could let them slide, to no good end, but then Matrix instances lose their functional nature.

MXAPI_ILLEGAL_MUTATE_NONCELL

In brief: no cell has been associated with that slot of this instance.

How the exception text is generated:

MXAPI_ILLEGAL_MUTATE_NONCELL> invalid mswap!/mset!/md-reset! to the property '" slot "', which is not mediated by any cell.
...> if such post-make mutation is in fact required, wrap the initial argument to model.core/make in 'cI'. eg: (make... :answer (cI 42)).
...> look for MXAPI_ILLEGAL_MUTATE_NONCELL in the Errors documentation for  more details.
...> FYI: intended new value is [" new-value "]; initial value was [" (get @me slot :no-such-slot) "].
...> FYI: instance is of type " (type-cljc me)
...> FYI: full instance is " @me
...> FYI: instance meta is " (meta me)

Discussion

The named slot exists, but the call to make did not wrap the slot's initial value in a cell. eg, we coded sth like (make :widget :answer 42). Such values cannot be changed later, for reasons to be discussed next, and this exception is thrown when that happens.

Reasons: In some dense dependency graphs, Matrix can make a nice performance optimization when it knows some property will never change in value, by eliminating dependency tracking of that property. Think const or defconstant in many languages. The Matrix API opts to make immutable values the default, partly because they are so much more common and partly so the few mutable properties of an application stand out.

Aside: the non-obvious win here is that Matrix applications are authored in a mostly declarative/functional fashion.

MXAPI_ILLEGAL_MUTATE_NONINPUT_CELL

In brief: the cell associated with this property of this instance was not declared with a truthy value for the cell property input?.

How the exception text is generated:

MXAPI_ILLEGAL_MUTATE_NONINPUT_CELL> invalid mswap!/mset!/md-reset! to the property '" (c-slot-name c) "', which is not mediated by an input cell.
..> if such post-make mutation is in fact required, wrap the initial argument to model.core/make in 'cI', 'cFn' or 'cF+n'. eg: (make... :answer (cFn <computation>)).
..> look for MXAPI_ILLEGAL_MUTATE_NONINPUT_CELL in the Matrix Errors documentation for  more details.
..> FYI: intended new value is [" new-value "]
..> FYI: the non-input cell is " @c 
..> FYI: instance is of type " (type-cljc me)
..> FYI: full instance is " @me
..> FYI: instance meta is " (meta me)

Discussion

If we want to mutate a property of a Matrix instance after it has been created, we must say so when creating the instance. We say that by wrapping an initial value in a cell whose property input? is a truthy value.

Two convenient API macros for this are cI and cFn, the latter being a cell that starts as a formula just long enough to compute a value from other cells, after which it behaves exactly as an input cell.

This error arises if we specify a rule when coding the make of some instance, eg, (make :ok-button :enabled? (cF (no-errors? me))) and we then try to toggle that value imperatively. If this happens, we just have to decide if the imperative case can be expressed functionally in the rule. If not, declare it as an input cell:

(make :ok-button :enabled? (cI false))

...then imperatively decide when to execute (mset! okbutton :enabled? true), and when to set it back to false, at which point we are back to manual state management.

MXAPI_COMPUTE_CYCLE_DETECTED

In brief: while computing cell C', Matrix internals see that C' is in the middle of computing. i.e., some dependency of C' requires C' directly or indirectly.

How the exception text is generated:

MXAPI_COMPUTE_CYCLE_DETECTED> cyclic dependency detected while computing slot 'slot ' of model '" (c-md-name c)
...> formula for " slot ":
   (c-code$ c)
...> full cell:
   @c
...> dependency stack, latest first:
   (str/join "\n" (mapv (fn [cd]
                    (str "....> md-name:" (c-md-name cd) " slot: "(c-slot-name cd)
                                "\n....>    code:" (c-code$ cd)))
                         *call-stack*)))

Discussion:

A dependency chain that cycles back to itself is rare in our experience. When it does occur, the problem is usually some over-broad dependency on too much information. A more specific dependency cures the problem.

A specific case of the above is when the computation of a component's children requires a property that in turn can be had only from one of the children. The solution here is much the same: avoid using the value to compute the children. That is rather vague, but when it arises we see that the children could indeed have been decided from less information.

MXAPI_UNDEFERRED_CHANGE

In brief: if observers want to mutate the Matrix in an observer/watch, they have to wrap the mutation in with-integrity.

How the exception text is generated:

MXAPI_UNDEFERRED_CHANGE> undeferred mswap!/mset!/md-reset! to the property '" slot "' by an observer detected.
...> such mutations must be wrapped by WITH-INTEGRITY, must conveniently with macro WITH-CC.
...> look for MXAPI_UNDEFERRED_CHANGE in the Errors documentation for  more details.
...> FYI: intended new value is [" new-value "]; current value is [" (get @me slot :no-such-slot) "].
...> FYI: instance is of type " (type-cljc me)
...> FYI: full instance is " @me
...> FYI: instance meta is " (meta me)

Discussion

We first recall that Matrix terminology uses the word "observer" for code that needs to know about a change, but which operates outside the Matrix dataflow.

A simple example is when a rule decides the border of a widget should be red because it is in error. When this happens, an observer must execute code to update the DOM, because the widget with the "border" property is just a proxy for the DOM element. There is no problem here because DOM manipulation per se is just a DOM API call -- no Matrix flow is involved. Other observer functionality is different; sometimes we do want an observer to be able to mess with Matrix flow. Example: perhaps an app can be more usable if, on occasion, it automatically triggers some change the user would otherwise have to execute themselves with the keyboard. A common example would be changing the page focus to a field that is in error, rather than merely highlighting it with a red border. Or when focusing on a field, we might automatically "select all" the contents so the user can just type in a new value to replace the errant value.

ie, We simply save the user a bit of effort. The code we want to execute might look like this in the first use case of auto-navigation to a field in error:

(defobserver error? [...]
   (mset! (focus page) error-widget))

The problem with the above code is that:

  • the page focus is probably a Matrix-powered property; and
  • the observer runs while the error? field is still being updated!

The problem with that is that it breaks the entire Matrix data integrity contract, which relies on every change being propagated fully before the next is made. Observers run as soon as a property changes, perhaps in a cascade of change following a single Matrix mset! mutation. So how do we effect a helpful change from within an observer?

Simple: the Matrix API was extended early on to support this common requirement. Defer any such mutation, enqueueing it for later execution, by wrapping it in the macro with-integrity, most conveniently with the macro with-cc:

(defobserver error? [...]
   (with-cc :autofocus-error
     (mset! (focus page) error-widget)))

So this error arises only as a developer oversight, failing to wrap mutations in observers in with-integrity.

Clone this wiki locally
0