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)
- Introduce the generics + typed components; migrate one distribution (
gamma) end-to-end; keep public accessors byte-compatible. Old switches delegate their gamma arm.
- One distribution per PR; delete each
switch arm as its file lands.
- When the last arm is gone, each
switch collapses to <generic>(new_dist(distribution)) and disappears.
- 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
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.
Motivation
Adding a distribution today is a scavenger hunt across
dist_spec.R: you edit thearg_matchenum, the CDFswitchindiscrete_pmf, and separateswitch(get_distribution(x), …)blocks formean,sd,max, andnatural_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).
switchstatements with S3 generics dispatched per distribution type.dist_spec.2. Class hierarchy — superclass stays
dist_spec.dist_specmust remain the superclass because nesting is detected by class (is(param, "dist_spec")) and it's the public contract (inherits(x, "dist_spec")). (Optional intermediatedistributionmarker for "single vs collection".)3. Dispatch on the object, buildable from a bare type.
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/sdreuse existing generics (meanis a base generic;sdis already made generic here), so single distributions definemean.gammaetc. directly.pdistrather thancdf: it returns the CDF function forprimarycensored(not evaluated values), andcdfwould collide with thedistributions3/distributionalgeneric.generaterather thansample: asamplegeneric would maskbase::sample.5. Discretisation is an optional capability, not a type split. A distribution is discretisable iff it provides
pdist; otherwisediscretise()errors clearly.6. Layout:
R/<name>.Rper distribution (constructor + methods); core class, combinators (c/+/==/print/plot) and the generics stay central;R/template.Rdoubles as the contributor doc.Migration (incremental, tests green throughout)
gamma) end-to-end; keep public accessors byte-compatible. Old switches delegate theirgammaarm.switcharm as its file lands.switchcollapses to<generic>(new_dist(distribution))and disappears.template.R.EpiNow2 impact
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_specclass) stays stable.Related
generategeneric).Migration checklist
new_dist), migrategammaend-to-end; old switches delegate. (Proof of concept — confirm the pattern before the rest.)normallognormalexpweibullbetafixednonparametric/dirichletswitches and remove thearg_matchenumtemplate.R+ "how to add a distribution" docgeneratemethod / sampler (tracked in Add a sampler for fixed-parameter dist_spec objects #28)EpiNow2 adoption (delete its
dist_spec.R, import distspec, fix the ~2natural_paramscall sites, reverse-dep check) is handled in EpiNow2's open PR, not here.