Forward properties
Utility functions
PortfolioOptimisers.@forward_properties — Macro
@forward_properties T begin
forward(loc)
forward(loc, names...)
alias(exposed, loc)
compute(exposed, loc; broadcast)
compute(exposed, fn)
swap(field, loc)
swap(field, fn)
endGenerate the Base.getproperty / Base.propertynames pair for type T from a block of declarative forwarding rules, so the property-forwarding decision lives in one declared surface instead of a hand-written getproperty body. T may be a bare type name or a parametric/UnionAll signature (Foo{<:Any, Nothing, <:Any}), so a swap can be specialised per type parameter.
All names are written as bare identifiers. Every rule names its source via a locator — a bare name a (the field a of the receiver) or a dotted path a.b.c (the receiver-rooted path obj.a.b.c, any depth). Nesting is simply more dots; a depth-≥2 path guards each intermediate and throws a PropertyPathError naming the path when a node is nothing.
forward/alias/compute only add new virtual names and so resolve after the receiver's own fields; swap replaces the value of an existing field and so resolves before the field check.
Rules
forward(loc): forward all properties of the value atloc(sym in propertynames(value)?getproperty(value, sym)).forward(loc, names...): forward only the named subset from the value atloc.alias(exposed, loc): exposeexposedas the value atloc(renaming).compute(exposed, loc; broadcast): exposeexposedvia a dotted locator (depth ≥ 2);broadcastmaps the final hop over a vector penultimate value.compute(exposed, fn): exposeexposedasfn(obj);fnmust be an anonymous function (a lambda), which would otherwise be ambiguous with a dotted path.swap(field, loc)/swap(field, fn): override an existing field's value with the value atloc(bare name, e.g.swap(L, M), or dotted path) or withfn(obj). Unlike the others it takes precedence over the own-field check, and is the only rule that may name a real field. Typically specialised on a parametricT(swap(L, M)onRegression{<:Any, Nothing, <:Any}). The locator form reads throughgetfieldand is recursion-safe; in the function form the body must read the swapped field viagetfield(obj, :field), neverobj.field, since dot-access on the swapped field re-entersgetpropertyand recurses (StackOverflowError). Other fields may use dot-access freely.
Algorithm
blockis not abegin … endblock: raise an error.- Make three empty vectors:
swap_branches,getprop_branchesandpropname_contribs. - For each rule of the block, skipping a
LineNumberNode:- The rule is not a call: raise an error naming it.
- Read
marker, the rule name, andargs, its arguments. When the first argument is a:parametersnode, read thebroadcastoption out of it and drop it fromargs. Any other option raises an error. markerisforward: flatten the locator withforward_flatten_pathand buildwalkwithforward_walk_expr. With no further argument, push a branch that returnsgetproperty(walk, sym)whensymis inpropertynames(walk), and contribute every one of those names. With further arguments, check that each is a bare identifier, push a branch that matchessymagainst that name set, and contribute the named subset.markerisalias: check the exposed name, buildwalkfrom the locator, push a branch that matches the exposed name and returnswalk, and contribute the name.markeriscompute: check the exposed name. An anonymous-function source pushes a branch returningfn(x), andbroadcastwith that form raises an error. A dotted source buildswalkwith thebroadcastflag and pushes the matching branch. Any other source raises an error. Contribute the exposed name.markerisswap: as forcompute, but a bare name is also a legal source, and the branch is pushed ontoswap_branchesrather thangetprop_branches.markeris anything else: raise an error naming it.
- Build
Base.getproperty(x::T, sym::Symbol)in this order: theswapbranches; the own-field check, which returnsgetfield(x, sym); the remaining branches in declaration order; andgetfield(x, sym)as the fallthrough, which raises the standard error for an absent field. - Build
Base.propertynames(x::T)fromfieldnames(T)followed by every contributed name, and return the unique names as a tuple. - Return both definitions in one escaped block.
Step 4 is where the two orderings in the first paragraph come from: a swap runs before the own-field check, so it replaces a real field, and every other rule runs after it, so it can only add a name. Within each group the first branch that matches wins, and the order of the branches is the declaration order of the rules.
Related