Skip to content

Refactor dist_spec.R into a per-distribution S3 interface #41

Description

@sbfnk-bot

Motivation

Adding a distribution today is a scavenger hunt across dist_spec.R: you edit the arg_match enum, the CDF switch in discrete_pmf, and separate switch(get_distribution(x), …) blocks for mean, sd, max, and natural_params/convert_to_natural, plus the constructor. The logic for one distribution is scattered "horizontally" across many functions.

Goal: everything about a distribution lives in its own file, so adding one is "copy the template, fill in the methods" — which also becomes the how to add a distribution documentation.

Design proposals

1. S3 interface + composition (not inheritance).

  • Replace the central switch statements with S3 generics dispatched per distribution type.
  • Nesting (a parameter that is itself a distribution) is composition, not inheritance — the generics recurse when a parameter is another dist_spec.

2. Class hierarchy — superclass stays dist_spec.

c("<type>", "dist_spec")            # a single distribution (interface dispatch target)
c("multi_dist_spec", "dist_spec")   # a combination

dist_spec must remain the superclass because nesting is detected by class (is(param, "dist_spec")) and it's the public contract (inherits(x, "dist_spec")). (Optional intermediate distribution marker for "single vs collection".)

3. Dispatch on the object, buildable from a bare type.

new_dist <- function(name, params = NULL) structure(list(params = params), class = c(name, "dist_spec"))

# type-level (ignore params)        # instance-level (use params)
natural_params.gamma <- function(d) c("shape", "rate")
pdist.gamma          <- function(d) pgamma
mean.gamma           <- function(d) d$params$shape / d$params$rate

4. Interface generics: natural_params, convert_to_natural, pdist (optional — the CDF function, for discretisation), mean, sd, validate, generate (RNG; addresses #28).

Naming notes:

  • mean/sd reuse existing generics (mean is a base generic; sd is already made generic here), so single distributions define mean.gamma etc. directly.
  • pdist rather than cdf: it returns the CDF function for primarycensored (not evaluated values), and cdf would collide with the distributions3/distributional generic.
  • generate rather than sample: a sample generic would mask base::sample.

5. Discretisation is an optional capability, not a type split. A distribution is discretisable iff it provides pdist; otherwise discretise() errors clearly.

6. Layout: R/<name>.R per distribution (constructor + methods); core class, combinators (c/+/==/print/plot) and the generics stay central; R/template.R doubles as the contributor doc.

Migration (incremental, tests green throughout)

  1. Introduce the generics + typed components; migrate one distribution (gamma) end-to-end; keep public accessors byte-compatible. Old switches delegate their gamma arm.
  2. One distribution per PR; delete each switch arm as its file lands.
  3. When the last arm is gone, each switch collapses to <generic>(new_dist(distribution)) and disappears.
  4. Write the "how to add a distribution" doc against template.R.

EpiNow2 impact

  • On adoption, the only external change is natural_params(string)natural_params(new_dist(type)) at ~2 call sites (summarise.R, get.R). Public API (constructors, discretise, fix_parameters, get_parameters, dist_spec class) stays stable.
  • Do this refactor before EpiNow2 adopts distspec, and run EpiNow2's suite as a reverse-dependency check before release.

Related

Migration checklist

  • Scaffolding + first distribution: introduce the interface generics + typed components (new_dist), migrate gamma end-to-end; old switches delegate. (Proof of concept — confirm the pattern before the rest.)
  • normal
  • lognormal
  • exp
  • weibull
  • beta
  • fixed
  • nonparametric / dirichlet
  • Collapse the now-empty switches and remove the arg_match enum
  • template.R + "how to add a distribution" doc
  • generate method / sampler (tracked in Add a sampler for fixed-parameter dist_spec objects #28)

EpiNow2 adoption (delete its dist_spec.R, import distspec, fix the ~2 natural_params call sites, reverse-dep check) is handled in EpiNow2's open PR, not here.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions