Skip to content

Support keyword arguments in @formula function calls#341

Open
sdwfrost wants to merge 1 commit intoJuliaStats:masterfrom
sdwfrost:formula-kwargs-support
Open

Support keyword arguments in @formula function calls#341
sdwfrost wants to merge 1 commit intoJuliaStats:masterfrom
sdwfrost:formula-kwargs-support

Conversation

@sdwfrost
Copy link

Summary

Allow keyword arguments in @formula function calls, enabling syntax like:

@formula(y ~ s(x; k=10, bs=:cr))
@formula(y ~ foo(x, a=1, b=2))

Currently, both forms crash in parse! because :kw and :parameters AST nodes
hit check_call() which requires head == :call. This PR adds handling for these
nodes so they pass through as quoted literals in FunctionTerm.args.

Motivation

Downstream packages that extend @formula with custom function terms (e.g., GAM
smooth specifications like s(x; k=10, bs=:cr)) need keyword arguments to configure
those terms. Without this change, users must use a separate macro (@gam_formula) or
encode configuration as positional arguments (cr(x, 10)), which is less readable
and less discoverable.

This is a common need — R's formula interface supports s(x, k=10, bs="cr") in mgcv,
and Julia's native call syntax already supports kwargs. The parser just needs to not
crash on them.

Changes

src/formula.jl

  • Add early-return parse! handling for :kw and :parameters Expr nodes — quotes
    them so they survive as literals in FunctionTerm.args
  • Add parse!(::QuoteNode, ...) method for quoted symbols (e.g., :cr)

src/terms.jl

  • Add kwarg_exprs(ft::FunctionTerm) — extract keyword argument expressions from
    FunctionTerm.exorig for downstream use
  • Add has_kwargs(ft::FunctionTerm) — convenience predicate

src/StatsModels.jl

  • Export kwarg_exprs, has_kwargs

test/formula.jl

  • 16 new tests covering semicolon kwargs, comma kwargs, mixed positional+kwargs,
    symbol values, numeric values, no-kwargs backward compatibility

Design

The approach is deliberately minimal and backward-compatible:

  1. No struct changesFunctionTerm already stores exorig (the original Expr),
    which naturally contains kwargs. We just need parse! to not crash before
    FunctionTerm is constructed.

  2. Kwargs are opaque to StatsModels — they pass through as quoted expressions.
    StatsModels does not interpret them. Downstream packages inspect them in their
    apply_schema implementations via kwarg_exprs(ft).

  3. Full backward compatibility — existing formulas produce identical results.
    All 965 existing tests pass unchanged.

Test Results

  • 981 tests pass (965 existing + 16 new)
  • 9 broken (all pre-existing)
  • 0 failures, 0 errors

Example Usage (downstream package)

using StatsModels

# Parse formula with kwargs
f = @formula(y ~ s(x; k=10, bs=:cr))

# Extract kwargs in apply_schema
function StatsModels.apply_schema(ft::FunctionTerm{typeof(s)}, schema, ...)
    kws = kwarg_exprs(ft)
    # kws = [:(k = 10), :(bs = :cr)]
    k = get_kwarg(kws, :k, 10)      # extract specific kwarg
    bs = get_kwarg(kws, :bs, :tp)
    # ... build smooth specification
end

Add parse! methods for :kw and :parameters Expr nodes so that
keyword arguments in function calls pass through instead of
erroring. This enables syntax like:

    @formula(y ~ s(x; k=10, bs=:cr))
    @formula(y ~ s(x, k=10, bs=:cr))

Kwargs are preserved in FunctionTerm.exorig and can be extracted
by downstream packages via the new kwarg_exprs() and has_kwargs()
helper functions.

This is fully backward compatible — existing formulas without
kwargs work identically. The 9 pre-existing broken tests are
unchanged.

Motivation: packages like GAM.jl need to pass configuration
options (basis type, dimension, etc.) to smooth term constructors
within formulas. Without this change, they must define custom
formula macros (@gam_formula) instead of using @formula.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant