diff --git a/docs/src/formula.md b/docs/src/formula.md index 34460f4a..1633fcc3 100644 --- a/docs/src/formula.md +++ b/docs/src/formula.md @@ -72,6 +72,54 @@ julia> Formula(StatsModels.Terms(@formula(y ~ 1 + (a+b) * c))) Formula: y ~ 1 + a + b + c + a & c + b & c ``` +### Constructing a formula programmatically + +Because a `Formula` is created at compile time with the `@formula` macro, +creating one programmatically means dipping into Julia's +[metaprogramming](https://docs.julialang.org/en/latest/manual/metaprogramming/) +facilities. + +Let's say you have a variable `lhs`: + +```jldoctest +julia> lhs = :y +:y +``` + +and you want to create a formula whose left-hand side is the _value_ of `lhs`, +as in + +```jldoctest +julia> @formula(y ~ 1 + x) +Formula: y ~ 1 + x +``` + +Simply using the Julia interpolation syntax `@formula($lhs ~ 1 + x)` won't work, +because `@formula` runs _at compile time_, before anything about the value of +`lhs` is known. Instead, you need to construct and evaluate the _correct call_ +to `@formula`. The most concise way to do this is with `@eval`: + +```jldoctest +julia> @eval @formula($lhs ~ 1 + x) +Formula: y ~ 1 + x +``` + +The `@eval` macro does two very different things in a single, convenient step: + +1. Generate a _quoted expression_ using `$`-interpolation to insert the run-time + value of `lhs` into the call to the `@formula` macro. +2. Evaluate this expression using `eval`. + +An equivalent but slightly more verbose way of doing the same thing is: + +```jldoctest +julia> formula_ex = :(@formula($lhs ~ 1 + x)) +:(@formula y ~ 1 + x) + +julia> eval(formula_ex) +Formula: y ~ 1 + x +``` + ### Technical details You may be wondering why formulas in Julia require a macro, while in R they diff --git a/src/formula.jl b/src/formula.jl index adf7935e..202e06b4 100644 --- a/src/formula.jl +++ b/src/formula.jl @@ -17,6 +17,9 @@ is_call(::Any) = false is_call(::Any, ::Any) = false check_call(ex) = is_call(ex) || throw(ArgumentError("non-call expression encountered: $ex")) +catch_dollar(ex::Expr) = + Meta.isexpr(ex, :$) && throw(ArgumentError("interpolation with \$ not supported in @formula. Use @eval @formula(...) instead.")) + mutable struct Formula ex_orig::Expr ex::Expr @@ -252,6 +255,7 @@ function parse!(i::Integer, rewrites) end function parse!(ex::Expr, rewrites::Vector) @debug "parsing $ex" + catch_dollar(ex) check_call(ex) # iterate over children, checking for special rules child_idx = 2