Skip to contents

frm_compat() answers what one feature does in the presence of another, and it answers in three states: the pair works, the pair is refused, or the pair is untested. A feature that lives outside frmtmb has to be able to say the same things about itself, or the matrix reports on part of the package and reads as if it reported on all of it.

Usage

frmtmb_register_compat(features = NULL, rules = NULL, expects = character(0))

compat_rule_builder()

compat_aterm_rules(accepts, existing = NULL)

Arguments

features

Named character vector mapping a feature's DISPLAY name to its kind: c("hmm" = "structure"), c("dec()" = "aterm"). The display name is how the feature is written in a formula or a call, parentheses included for a callable one; the kind groups it in the printed matrix and decides which kind-level rules reach it, and is one of "family", "covstruct", "aterm", "special", "autocor", "mode", "structure", "method", "grammar". A name the registry already carries under the same kind is a no-op, so a reload is not an error and neither is declaring an addition term that frmtmb_register_aterm() has already declared for you.

rules

A FUNCTION of no arguments returning a rule data frame, not the data frame itself, so that contributed rules are built on demand exactly as the core ones are. Build the frame with compat_rule_builder(): it returns a list of two functions, r(a, b, status, note, override = FALSE) to record one rule and rules() to return the accumulated frame. a and b are display names, "*" matches every feature, and override = TRUE says the rule beats a more specific one rather than losing to it.

expects

Character vector of feature names the rules refer to but this package does not supply, because another package does. They are exempt from the check below and are NOT added to the vocabulary: a rule naming one of them lies dormant until the package that owns the feature is loaded, which is what makes the rule true. Use it for that and nothing else. A feature in the table that nothing implements is the failure the registry exists to prevent.

A name the vocabulary already holds is refused, because the declaration then exempts nothing: give it a kind in features =, or drop it. A name still unresolved when frm_compat() is called is reported there, in the unresolved attribute the print method shows, so a rule waiting for a package that never arrives is visible rather than silent. That is what makes a misspelling inside expects findable: the check here cannot catch one, since the whole point is that the owner is absent.

The near-miss guess in a refusal is searched over the vocabulary only, never over expects, so a misspelling of an expected name is refused without a suggestion. Pointing at a feature that is not in the session would be its own confusion.

accepts

For compat_aterm_rules(): a named list mapping a family's DISPLAY name to what that family accepts. An element is a family object, a character vector of addition-term names without parentheses (the vocabulary frmtmb_family(accepts_aterms =) is written in), or NULL for a family that declares no allow-list and so has nothing to derive from.

existing

For compat_aterm_rules(): the rules written by hand, so that the derivation defers to them. A pair named on both sides there is left alone, note and all. NULL defers to nothing.

Value

NULL, invisibly. Called for the registration. compat_rule_builder() returns a list with elements r and rules. compat_aterm_rules() returns a rule data frame, ready to rbind() with one.

Details

frmtmb_register_compat() is that seam. Call it from the contributing package's .onLoad(): by then every namespace is sealed, so the collation-order question a top-level call would raise does not arise. compat_rule_builder() is the accumulator the core's own rules are written with, exported so that a contributed rule reads the same as a core one.

Contributions are APPENDED, which is the only safe direction. Rules of equal specificity resolve later-wins, so a contributed rule may override a core default and a core default can never silently override a contributed one. Between packages, contributions resolve in the order of the packages' names, so the table does not depend on the order in which they were loaded.

What registration refuses

A rule side names a feature, a whole kind ("kind:family"), a named group ("group:cdf"), or everything ("*"). A side that names none of those matches no pair, and a rule that matches no pair is dropped without a word: the contributing package's table then reads as complete while covering less than it claims, which is the worst thing a compatibility surface can do. So registration refuses it, naming the rule, the spelling, and the nearest entry in the vocabulary when one is close.

The check runs against the vocabulary AFTER features is added, so a package names its own new features freely in its own rules. It runs at registration, which means rules is called once there: keep it a pure builder that reads nothing but its own arguments.

An addition term registered with frmtmb_register_aterm() is added to the vocabulary for you, as "<name>()" of kind "aterm". A registered term the table cannot describe would be a gap by construction, so the registrant is not asked to say it twice; saying it anyway is a no-op. Register the term BEFORE the rules that name it.

Rows a family has already declared

compat_aterm_rules() writes the addition-term refusals out of frmtmb_family(accepts_aterms =) instead of asking for them twice. A term outside a family's allow-list is refused BY NAME at frame assembly, so untested was never the right answer for that cell: nothing is missing, the guard exists and the reason is the declaration. Feed it the families the package supplies, and hand it the rows written by hand so that it defers to them:

hand <- b$rules()
rbind(hand,
      compat_aterm_rules(list(wiener = c("dec", "vint", "weights")),
                         hand))

A pair existing already names on both sides is skipped, note and all, so the derivation adds cells rather than replacing them. What is derived is only the refusal: a family that ACCEPTS a term has said nothing about whether the pair works, and untested stays the honest answer there until somebody runs it.

Status vocabulary

Every rule declares one of three states, and the third is the reason the registry exists: a guard that does not exist looks exactly like a guard that passed, so "no error" was never evidence of support.

"works"

The pair is supported and exercised by a test.

"refused"

The pair is rejected with a message. Say in the note what the user should do instead.

"untested"

Nothing is known. Neither a promise nor a refusal.

What a refused row promises

A refused row is a statement about what the code DOES, not a note about what a reader should avoid. The exact promise is: a refused row is enforced wherever the enforcing code can see the feature, and that code says what it can see. A row that says refused while a guard that could have seen the feature lets the pair run is a defect of the same size as a missing guard.

The qualification is not a hedge, it is where the two kinds of enforcement differ. Most of core's refusals are hand-written guards that the row describes, raised where the frame is assembled or where the method is called; those see everything, so for them the promise is unqualified. The other kind is enforced by CONSULTING this registry at the point of use: the caller queries the rows naming its own feature, decides whether the model in front of it uses the other side, and raises with the registered note verbatim. Deciding that is the hard half, because what identifies a feature in a model is usually its POSITION in the formula, which the parser owns.

frmtmb.sample::frm_sample() is the first caller of that kind and is worth reading as the worked example. It queries the rows whose feature is frm_sample and whose status is refused before it tapes anything, so a package that owns a model the sampler cannot run declares one row and needs no code in the sampler; frmtmb.ode does exactly that. It also documents the rows it cannot decide, and warns rather than refusing on those, because a check that fires on a correct model is worse than no check. So a registrant who needs a refusal ENFORCED rather than only reported should read the caller's own documentation and confirm the caller can see the feature; a row the caller cannot see is still true, and is still reported by frm_compat(), but it will not stop anything.

See also

frm_compat() for the matrix these fill, frmtmb_family() and frmtmb_structure() for the family-side seams a contributor usually registers alongside these, and vignette("compatibility")

Examples

# what a contributing package's .onLoad() does
contribute <- function() {
  b <- compat_rule_builder()
  b$r("wiener", "cens()", "refused",
      "The family supplies no lcdf, so there is no CDF to censor with.")
  b$r("wiener", "*", "untested",
      "Not exercised outside this package's own suite.")
  b$r("wiener", "hmm", "untested",
      "Another package's feature, so the rule waits for it.")
  frmtmb_register_compat(features = c("wiener" = "family"),
                         rules = b$rules, expects = "hmm")
}
# the accumulator on its own, which is all a rule set is
b <- compat_rule_builder()
b$r("dec()", "trials()", "refused", "Different response shapes.")
b$rules()
#>   feature_a feature_b  status                       note override
#> 1     dec()  trials() refused Different response shapes.    FALSE