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)
end

Generate 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 at loc (sym in propertynames(value) ? getproperty(value, sym)).
  • forward(loc, names...): forward only the named subset from the value at loc.
  • alias(exposed, loc): expose exposed as the value at loc (renaming).
  • compute(exposed, loc; broadcast): expose exposed via a dotted locator (depth ≥ 2); broadcast maps the final hop over a vector penultimate value.
  • compute(exposed, fn): expose exposed as fn(obj); fn must 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 at loc (bare name, e.g. swap(L, M), or dotted path) or with fn(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 parametric T (swap(L, M) on Regression{<:Any, Nothing, <:Any}). The locator form reads through getfield and is recursion-safe; in the function form the body must read the swapped field via getfield(obj, :field), never obj.field, since dot-access on the swapped field re-enters getproperty and recurses (StackOverflowError). Other fields may use dot-access freely.

Algorithm

  1. block is not a begin … end block: raise an error.
  2. Make three empty vectors: swap_branches, getprop_branches and propname_contribs.
  3. For each rule of the block, skipping a LineNumberNode:
    1. The rule is not a call: raise an error naming it.
    2. Read marker, the rule name, and args, its arguments. When the first argument is a :parameters node, read the broadcast option out of it and drop it from args. Any other option raises an error.
    3. marker is forward: flatten the locator with forward_flatten_path and build walk with forward_walk_expr. With no further argument, push a branch that returns getproperty(walk, sym) when sym is in propertynames(walk), and contribute every one of those names. With further arguments, check that each is a bare identifier, push a branch that matches sym against that name set, and contribute the named subset.
    4. marker is alias: check the exposed name, build walk from the locator, push a branch that matches the exposed name and returns walk, and contribute the name.
    5. marker is compute: check the exposed name. An anonymous-function source pushes a branch returning fn(x), and broadcast with that form raises an error. A dotted source builds walk with the broadcast flag and pushes the matching branch. Any other source raises an error. Contribute the exposed name.
    6. marker is swap: as for compute, but a bare name is also a legal source, and the branch is pushed onto swap_branches rather than getprop_branches.
    7. marker is anything else: raise an error naming it.
  4. Build Base.getproperty(x::T, sym::Symbol) in this order: the swap branches; the own-field check, which returns getfield(x, sym); the remaining branches in declaration order; and getfield(x, sym) as the fallthrough, which raises the standard error for an absent field.
  5. Build Base.propertynames(x::T) from fieldnames(T) followed by every contributed name, and return the unique names as a tuple.
  6. 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

source