VarNames and optics
VarNames: an overview
One of the most important parts of AbstractPPL.jl is the VarName type, which is used throughout the TuringLang ecosystem to represent names of random variables.
Fundamentally, a VarName comprises a symbol (which represents the name of the variable itself) and an optic (which tells us which part of the variable we might be interested in). For example, x.a[1] means the first element of the field a of the variable x. Here, x is the symbol, and .a[1] is the optic.
VarNames can be created using the @varname macro:
using AbstractPPL
vn = @varname(x.a[1])x.a[1]AbstractPPL.VarName — Type
VarName{sym}(optic=identity)A variable identifier for a symbol sym and optic optic. sym refers to the name of the top-level Julia variable, while optic allows one to specify a particular property or index inside that variable.
VarNames can be manually constructed using the VarName{sym}(optic) constructor, or from an optic expression through the @varname convenience macro.
AbstractPPL.@varname — Macro
@varname(expr, concretize=false)Create a VarName given an expression expr representing a variable or part of it.
Basic examples
In general, VarNames must have a top-level symbol representing the identifier itself, and can then have any number of property accesses or indexing operations chained to it.
julia> @varname(x)
x
julia> @varname(x.a.b.c)
x.a.b.c
julia> @varname(x[1][2][3])
x[1][2][3]
julia> @varname(x.a[1:3].b[2])
x.a[1:3].b[2]Dynamic indices
Some expressions may involve dynamic indices, specifically, begin, end. These indices cannot be resolved, or 'concretized', until the value being indexed into is known. By default, @varname(...) will not automatically concretize these expressions, and thus the resulting VarName will contain markers for these.
Note that colons are not considered dynamic.
julia> vn = @varname(x[end])
x[DynamicIndex(end)]
julia> vn = @varname(x[1, end-1])
x[1, DynamicIndex(end - 1)]You can detect whether a VarName contains any dynamic indices using is_dynamic:
julia> vn = @varname(x[1, end-1]); AbstractPPL.is_dynamic(vn)
trueTo concretize such expressions, you can call concretize on the resulting VarName. After concretization, the resulting VarName will no longer be dynamic.
julia> x = randn(2, 3);
julia> vn = @varname(x[1, end-1]); vn2 = AbstractPPL.concretize(vn, x)
x[1, 2]
julia> getoptic(vn2).ix # Just an ordinary tuple.
(1, 2)
julia> AbstractPPL.is_dynamic(vn2)
falseAlternatively, you can pass true as the second positional argument to the @varname macro (note that it is not a keyword argument!). This will automatically call concretize for you, using the top-level symbol to look up the value used for concretization.
julia> x = randn(2, 3);
julia> @varname(x[1:end, end][:], true)
x[1:2, 3][:]Interpolation
Property names, as well as top-level symbols, can also be constructed from interpolated symbols:
julia> name = :hello; @varname(x.$name)
x.hello
julia> @varname($name)
hello
julia> @varname($name.a.$name[1])
hello.a.hello[1]For indices, you do not need to use $ to interpolate, just use the variable directly:
julia> ix = 2; @varname(x[ix])
x[2]Note that if the top-level symbol is interpolated, automatic concretization is not possible:
julia> name = :x; @varname($name[1:end], true)
ERROR: LoadError: cannot automatically concretize VarName with interpolated top-level symbol; call `concretize(vn, val)` manually instead
[...]AbstractPPL.varname — Function
varname(expr, concretize::Bool)Implementation of the @varname macro. See the documentation for @varname for details. This function is exported to allow other macros (e.g. in DynamicPPL) to reuse the same logic.
You can obtain the components of a VarName using the getsym and getoptic functions:
getsym(vn), getoptic(vn)(:x, Optic(.a[1]))AbstractPPL.getsym — Function
getsym(vn::VarName)Return the symbol of the Julia variable used to generate vn.
Examples
julia> getsym(@varname(x[1][2:3]))
:x
julia> getsym(@varname(y))
:yAbstractPPL.getoptic — Function
getoptic(vn::VarName)Return the optic of the Julia variable used to generate vn.
Examples
julia> getoptic(@varname(x[1][2:3]))
Optic([1][2:3])
julia> getoptic(@varname(y))
Optic()Dynamic indices
VarNames may contain 'dynamic' indices, that is, indices whose meaning is not known until they are resolved against a specific value. For example, x[end] refers to the last element of x; but we don't know what that means until we know what x is.
Specifically, begin and end symbols in indices are treated as dynamic indices. This is also true for any expression that contains begin or end, such as end-1 or 1:3:end.
Dynamic indices are represented using an internal type, AbstractPPL.DynamicIndex.
vn_dyn = @varname(x[1:2:end])x[DynamicIndex(1:2:end)]You can detect whether a VarName contains dynamic indices using the is_dynamic function:
is_dynamic(vn_dyn)trueAbstractPPL.is_dynamic — Function
is_dynamic(vn::VarName)Return true if vn contains any dynamic indices (i.e., begin, end, or :). If a VarName has been concretized, this will always return false.
These dynamic indices can be resolved, or concretized, by passing a specific value to the concretize function:
x = randn(5)
vn_conc = concretize(vn_dyn, x)x[1:2:5]AbstractPPL.concretize — Function
concretize(vn::VarName, x)Return vn concretized on x, i.e. any information related to the runtime shape of x is evaluated. This will convert any begin and end indices in vn to concrete indices with information about the length of the dimension being indexed into.
Optics
The optics used in AbstractPPL.jl are represented as a linked list. For example, the optic .a[1] is a Property optic that contains an Index optic as its child. That means that the 'elements' of the linked list can be read from left-to-right:
Property{:a} -> Index{1} -> IdenAll optic linked lists are terminated with an Iden optic, which represents the identity function.
optic = getoptic(@varname x.a[1])
dump(optic)Property{:a, Index{Tuple{Int64}, @NamedTuple{}, Iden}}
child: Index{Tuple{Int64}, @NamedTuple{}, Iden}
ix: Tuple{Int64}
1: Int64 1
kw: @NamedTuple{} NamedTuple()
child: Iden Optic()AbstractPPL.AbstractOptic — Type
AbstractOpticAn abstract type that represents the non-symbol part of a VarName, i.e., the section of the variable that is of interest. For example, in x.a[1][2], the AbstractOptic represents the .a[1][2] part.
AbstractPPL.Property — Type
Property{sym}(child=Iden())A property access optic representing access to property sym. A VarName{:x} with this optic represents access to x.sym. The child optic represents any further indexing or property access after this property access operation.
AbstractPPL.Index — Type
Index(ix, kw, child=Iden())An indexing optic representing access to indices ix, which may also take the form of keyword arguments kw. A VarName{:x} with this optic represents access to x[ix..., kw...]. The child optic represents any further indexing or property access after this indexing operation.
AbstractPPL.Iden — Type
Iden()The identity optic. This is the optic used when we are referring to the entire variable. It is also the base case for composing optics.
Instead of calling getoptic(@varname(...)), you can directly use the @opticof macro to create optics:
optic = @opticof(_.a[1])Optic(.a[1])AbstractPPL.@opticof — Macro
@opticof(expr, concretize=false)Extract the optic from @varname(expr, concretize). This is a thin wrapper around getoptic(@varname(...)).
If you don't need to concretize, you should use _ as the top-level symbol to indicate that it is not relevant:
julia> @opticof(_.a.b)
Optic(.a.b)If you need to concretize, then you can provide a real variable name (which is then used to look up the value for concretization):
julia> x = randn(3, 4); @opticof(x[1:end, end], true)
Optic([1:3, 4])Note that concretization with @opticof has the same limitations as with @varname, specifically, if the top-level symbol is interpolated, automatic concretization is not possible.
Getting and setting
Optics are callable structs, and when passed a value will extract the relevant part of that value.
data = (a=[10, 20, 30], b="hello")
optic = @opticof(_.a[2])
optic(data)20You can set values using Accessors.set (which AbstractPPL re-exports). Note, though, that this will not mutate the original value. Furthermore, you cannot use the handy macros like Accessors.@set, since those will use the optics from Accessors.jl.
new_data = set(data, optic, 99)
new_data, data((a = [10, 99, 30], b = "hello"), (a = [10, 20, 30], b = "hello"))If you want to try to mutate values, you can wrap an optic using with_mutation.
optic_mut = with_mutation(optic)
set(data, optic_mut, 99)
data(a = [10, 99, 30], b = "hello")AbstractPPL.with_mutation — Function
with_mutation(o::AbstractOptic)Create a version of the optic o which attempts to mutate its input where possible.
On their own, AbstractOptics are non-mutating:
julia> optic = @opticof(_[1])
Optic([1])
julia> x = [0.0, 0.0];
julia> set(x, optic, 1.0); x
2-element Vector{Float64}:
0.0
0.0With this function, we can create a mutating version of the optic:
julia> optic_mut = with_mutation(@opticof(_[1]))
Optic!!([1])
julia> x = [0.0, 0.0];
julia> set(x, optic_mut, 1.0); x
2-element Vector{Float64}:
1.0
0.0Thanks to the BangBang.jl package, this optic will gracefully fall back to non-mutating behaviour if mutation is not possible. For example, if we try to use it on a tuple:
julia> optic_mut = with_mutation(@opticof(_[1]))
Optic!!([1])
julia> x = (0.0, 0.0);
julia> set(x, optic_mut, 1.0); x
(0.0, 0.0)Composing and decomposing optics
If you have two optics, you can compose them using the ∘ operator:
optic1 = @opticof(_.a)
optic2 = @opticof(_[1])
composed = optic2 ∘ optic1Optic(.a[1])Notice the order of composition here, which can be counterintuitive: optic2 ∘ optic1 means "first apply optic1, then apply optic2", and thus this represents the optic .a[1] (not .[1].a).
Base.cat(optics...) is also provided, which composes optics in a more intuitive sense (indeed, if you think of an optic as a linked list, this can be thought of as concatenating the lists). The following is equivalent to the previous example:
composed2 = Base.cat(optic1, optic2)Optic(.a[1])Several functions are provided to decompose optics, which all stem from their linked-list structure. Their names directly mirror Haskell's functions for decomposing lists, but are prefixed with o:
AbstractPPL.ohead — Function
ohead(optic::AbstractOptic)Get the innermost layer of an optic. For all optics, we have that otail(optic) ∘ ohead(optic) == optic.
julia> ohead(@opticof _.a[1][2])
Optic(.a)
julia> ohead(@opticof _)
Optic()AbstractPPL.otail — Function
otail(optic::AbstractOptic)Get everything but the innermost layer of an optic. For all optics, we have that otail(optic) ∘ ohead(optic) == optic.
julia> otail(@opticof _.a[1][2])
Optic([1][2])
julia> otail(@opticof _)
Optic()AbstractPPL.oinit — Function
oinit(optic::AbstractOptic)Get everything but the outermost layer of an optic. For all optics, we have that olast(optic) ∘ oinit(optic) == optic.
julia> oinit(@opticof _.a[1][2])
Optic(.a[1])
julia> oinit(@opticof _)
Optic()AbstractPPL.olast — Function
olast(optic::AbstractOptic)Get the outermost layer of an optic. For all optics, we have that olast(optic) ∘ oinit(optic) == optic.
julia> olast(@opticof _.a[1][2])
Optic([2])
julia> olast(@opticof _)
Optic()For example, ohead returns the first element of the optic linked list, and otail returns the rest of the list after removing the head:
optic = @opticof(_.a[1].b[2])
ohead(optic), otail(optic)(Optic(.a), Optic([1].b[2]))Convesely, oinit returns the optic linked list without its last element, and olast returns the last element:
oinit(optic), olast(optic)(Optic(.a[1].b), Optic([2]))If the optic only has a single element, then oinit and otail return Iden, while ohead and olast return the optic itself:
optic_single = @opticof(_.a)
oinit(optic_single), olast(optic_single), ohead(optic_single), otail(optic_single)(Optic(), Optic(.a), Optic(.a), Optic())Converting VarNames to optics and back
Sometimes it is useful to treat a VarName's top level symbol as if it were part of the optic. For example, when indexing into a NamedTuple nt, we might want to treat the entire VarName x.a[1] as an optic that can be applied to a NamedTuple: i.e., we want to access the nt.x field rather than the variable x itself. This can be achieved with:
AbstractPPL.varname_to_optic — Function
varname_to_optic(vn::VarName)Convert a VarName to an optic, by converting the top-level symbol to a Property optic.
AbstractPPL.optic_to_varname — Function
optic_to_varname(optic::Property{sym}) where {sym}Convert a Property optic to a VarName, by converting the top-level property to a symbol. This fails for all other optics.
Subsumption
Sometimes, we want to check whether one VarName 'subsumes' another; that is, whether a VarName refers to a part of another VarName. This is done using the subsumes function:
vn1 = @varname(x.a)
vn2 = @varname(x.a[1])
subsumes(vn1, vn2)trueAbstractPPL.subsumes — Function
subsumes(parent::VarName, child::VarName)Check whether the variable name child describes a sub-range of the variable parent, i.e., is contained within it.
julia> subsumes(@varname(x), @varname(x[1, 2]))
true
julia> subsumes(@varname(x[1, 2]), @varname(x[1, 2][3]))
trueThis is done by recursively comparing each layer of the VarNames' optics.
Note that often this is not possible to determine statically, and so the results should not be over-interpreted. In particular, Index optics pose a problem. An i::Index will only subsume j::Index if:
- They have the same number of positional indices (
i.ixandj.ix); - Each positional index in
ican be determined to comprise the corresponding positional index inj; and - The keyword indices of
i(i.kw) are a superset of those inj.kw).
In all other cases, subsumes will conservatively return false, even though in practice it might well be that i does subsume j. Some examples where subsumption cannot be determined statically are:
- Subsumption between different forms of indexing is not supported, e.g.
x[4]andx[2, 2]are not considered to subsume each other, even though they might in practice (e.g. ifxis a 2x2 matrix). - When dynamic indices (that are not equal) are present. (Dynamic indices that are equal do subsume each other.)
- Non-standard indices, e.g.
Not(4),2..3, etc. Again, these only subsume each other when they are equal.
Prefixing and unprefixing
Composing two optics can be done using the ∘ operator, as shown above. But what if we want to compose two VarNames? This is used, for example, in DynamicPPL's submodel functionality.
AbstractPPL.prefix — Function
prefix(vn::VarName, prefix::VarName)Add a prefix to a VarName.
julia> prefix(@varname(x), @varname(y))
y.x
julia> prefix(@varname(x.a), @varname(y))
y.x.a
julia> prefix(@varname(x.a), @varname(y[1]))
y[1].x.aAbstractPPL.unprefix — Function
unprefix(vn::VarName, prefix::VarName)Remove a prefix from a VarName.
julia> unprefix(@varname(y.x), @varname(y))
x
julia> unprefix(@varname(y.x.a), @varname(y))
x.a
julia> unprefix(@varname(y[1].x), @varname(y[1]))
x
julia> unprefix(@varname(y), @varname(n))
ERROR: ArgumentError: cannot remove prefix n from VarName y
[...]
julia> unprefix(@varname(y[1]), @varname(y))
ERROR: ArgumentError: optic_to_varname: can only convert Property optics to VarName
[...]VarName leaves
The following functions are used to extract the 'leaves' of a VarName, that is, the atomic components of a VarName that do not have any further substructure. For example, for a vector variable x, the leaves would be x[1], x[2], etc.
AbstractPPL.varname_leaves — Function
varname_leaves(vn::VarName, val)Return an iterator over all varnames that are represented by vn on val.
Examples
julia> using AbstractPPL: varname_leaves
julia> foreach(println, varname_leaves(@varname(x), rand(2)))
x[1]
x[2]
julia> foreach(println, varname_leaves(@varname(x[1:2]), rand(2)))
x[1:2][1]
x[1:2][2]
julia> x = (y = 1, z = [[2.0], [3.0]]);
julia> foreach(println, varname_leaves(@varname(x), x))
x.y
x.z[1][1]
x.z[2][1]AbstractPPL.varname_and_value_leaves — Function
varname_and_value_leaves(vn::VarName, val)Return an iterator over all varname-value pairs that are represented by vn on val.
Examples
julia> using AbstractPPL: varname_and_value_leaves
julia> foreach(println, varname_and_value_leaves(@varname(x), 1:2))
(x[1], 1)
(x[2], 2)
julia> foreach(println, varname_and_value_leaves(@varname(x[1:2]), 1:2))
(x[1:2][1], 1)
(x[1:2][2], 2)
julia> x = (y = 1, z = [[2.0], [3.0]]);
julia> foreach(println, varname_and_value_leaves(@varname(x), x))
(x.y, 1)
(x.z[1][1], 2.0)
(x.z[2][1], 3.0)There is also some special handling for certain types:
julia> using LinearAlgebra
julia> x = reshape(1:4, 2, 2);
julia> # `LowerTriangular`
foreach(println, varname_and_value_leaves(@varname(x), LowerTriangular(x)))
(x[1, 1], 1)
(x[2, 1], 2)
(x[2, 2], 4)
julia> # `UpperTriangular`
foreach(println, varname_and_value_leaves(@varname(x), UpperTriangular(x)))
(x[1, 1], 1)
(x[1, 2], 3)
(x[2, 2], 4)
julia> # `Cholesky` with lower-triangular
foreach(println, varname_and_value_leaves(@varname(x), Cholesky([1.0 0.0; 0.0 1.0], 'L', 0)))
(x.L[1, 1], 1.0)
(x.L[2, 1], 0.0)
(x.L[2, 2], 1.0)
julia> # `Cholesky` with upper-triangular
foreach(println, varname_and_value_leaves(@varname(x), Cholesky([1.0 0.0; 0.0 1.0], 'U', 0)))
(x.U[1, 1], 1.0)
(x.U[1, 2], 0.0)
(x.U[2, 2], 1.0)varname_and_value_leaves(container)Return an iterator over all varname-value pairs that are represented by container.
This is the same as varname_and_value_leaves(vn::VarName, x) but over a container containing multiple varnames.
See also: varname_and_value_leaves(vn::VarName, x).
Examples
julia> using AbstractPPL: varname_and_value_leaves
julia> using OrderedCollections: OrderedDict
julia> # With an `AbstractDict` (we use `OrderedDict` here
# to ensure consistent ordering in doctests)
dict = OrderedDict(@varname(y) => 1, @varname(z) => [[2.0], [3.0]]);
julia> foreach(println, varname_and_value_leaves(dict))
(y, 1)
(z[1][1], 2.0)
(z[2][1], 3.0)
julia> # With a `NamedTuple`
nt = (y = 1, z = [[2.0], [3.0]]);
julia> foreach(println, varname_and_value_leaves(nt))
(y, 1)
(z[1][1], 2.0)
(z[2][1], 3.0)Reading from a container with a VarName (or optic)
AbstractPPL.canview — Function
canview(optic, container)Return true if optic can be used to view container, and false otherwise.
Examples
julia> AbstractPPL.canview(@opticof(_.a), (a = 1.0, ))
true
julia> AbstractPPL.canview(@opticof(_.a), (b = 1.0, )) # property `a` does not exist
false
julia> AbstractPPL.canview(@opticof(_.a[1]), (a = [1.0, 2.0], ))
true
julia> AbstractPPL.canview(@opticof(_.a[3]), (a = [1.0, 2.0], )) # out of bounds
falseAbstractPPL.hasvalue — Function
hasvalue(
vals::Union{AbstractDict,NamedTuple},
vn::VarName,
dist::Distribution;
error_on_incomplete::Bool=false
)Check if vals contains values for vn that is compatible with the distribution dist.
This is a more general version of hasvalue(vals, vn), in that even if vn itself is not inside vals, it further checks if vals contains sub-values of vn that can be used to reconstruct vn given dist.
The error_on_incomplete flag can be used to detect cases where some of the values needed for vn are present, but others are not. This may help to detect invalid cases where the user has provided e.g. data of the wrong shape.
Note that this check is only possible if a Dict is passed, because the key type of a NamedTuple (i.e., Symbol) is not rich enough to carry indexing information. If this method is called with a NamedTuple, it will just defer to hasvalue(vals, vn).
For example:
julia> d = Dict(@varname(x[1]) => 1.0, @varname(x[2]) => 2.0);
julia> hasvalue(d, @varname(x), MvNormal(zeros(2), I))
true
julia> hasvalue(d, @varname(x), MvNormal(zeros(3), I))
false
julia> hasvalue(d, @varname(x), MvNormal(zeros(3), I); error_on_incomplete=true)
ERROR: only partial values for `x` found in the dictionary provided
[...]hasvalue(vals::NamedTuple, vn::VarName)
hasvalue(vals::AbstractDict{<:VarName}, vn::VarName)Determine whether vals contains a value for a given vn.
Examples
With x as a NamedTuple:
julia> hasvalue((x = 1.0, ), @varname(x))
true
julia> hasvalue((x = 1.0, ), @varname(x[1]))
false
julia> hasvalue((x = [1.0],), @varname(x))
true
julia> hasvalue((x = [1.0],), @varname(x[1]))
true
julia> hasvalue((x = [1.0],), @varname(x[2]))
falseWith x as a AbstractDict:
julia> hasvalue(Dict(@varname(x) => 1.0, ), @varname(x))
true
julia> hasvalue(Dict(@varname(x) => 1.0, ), @varname(x[1]))
false
julia> hasvalue(Dict(@varname(x) => [1.0]), @varname(x))
true
julia> hasvalue(Dict(@varname(x) => [1.0]), @varname(x[1]))
true
julia> hasvalue(Dict(@varname(x) => [1.0]), @varname(x[2]))
falseIn the AbstractDict case we can also have keys such as v[1]:
julia> vals = Dict(@varname(x[1]) => [1.0,]);
julia> hasvalue(vals, @varname(x[1])) # same as `haskey`
true
julia> hasvalue(vals, @varname(x[1][1])) # different from `haskey`
true
julia> hasvalue(vals, @varname(x[1][2]))
false
julia> hasvalue(vals, @varname(x[2][1]))
falseAbstractPPL.getvalue — Function
getvalue(
vals::Union{AbstractDict,NamedTuple},
vn::VarName,
dist::Distribution
)Retrieve the value of vn from vals, using the distribution dist to reconstruct the value if necessary.
This is a more general version of getvalue(vals, vn), in that even if vn itself is not inside vals, it can still reconstruct the value of vn from sub-values of vn that are present in vals.
Note that this reconstruction is only possible if a Dict is passed, because the key type of a NamedTuple (i.e., Symbol) is not rich enough to carry indexing information. If this method is called with a NamedTuple, it will just defer to getvalue(vals, vn).
For example:
julia> d = Dict(@varname(x[1]) => 1.0, @varname(x[2]) => 2.0);
julia> getvalue(d, @varname(x), MvNormal(zeros(2), I))
2-element Vector{Float64}:
1.0
2.0
julia> # Use `hasvalue` to check for this case before calling `getvalue`.
getvalue(d, @varname(x), MvNormal(zeros(3), I))
ERROR: `x` was not found in the dictionary provided
[...]getvalue(vals::NamedTuple, vn::VarName)
getvalue(vals::AbstractDict{<:VarName}, vn::VarName)Return the value(s) in vals represented by vn.
Examples
For NamedTuple:
julia> vals = (x = [1.0],);
julia> getvalue(vals, @varname(x)) # same as `getindex`
1-element Vector{Float64}:
1.0
julia> getvalue(vals, @varname(x[1])) # different from `getindex`
1.0
julia> getvalue(vals, @varname(x[2]))
ERROR: x[2] was not found in the NamedTuple provided
[...]For AbstractDict:
julia> vals = Dict(@varname(x) => [1.0]);
julia> getvalue(vals, @varname(x)) # same as `getindex`
1-element Vector{Float64}:
1.0
julia> getvalue(vals, @varname(x[1])) # different from `getindex`
1.0
julia> getvalue(vals, @varname(x[2]))
ERROR: x[2] was not found in the dictionary provided
[...]In the AbstractDict case we can also have keys such as v[1]:
julia> vals = Dict(@varname(x[1]) => [1.0,]);
julia> getvalue(vals, @varname(x[1])) # same as `getindex`
1-element Vector{Float64}:
1.0
julia> getvalue(vals, @varname(x[1][1])) # different from `getindex`
1.0
julia> getvalue(vals, @varname(x[1][2]))
ERROR: x[1][2] was not found in the dictionary provided
[...]
julia> getvalue(vals, @varname(x[2][1]))
ERROR: x[2][1] was not found in the dictionary provided
[...]
julia> getvalue(vals, @varname(x))
ERROR: x was not found in the dictionary provided
[...]Dictionaries can present ambiguous cases where the same variable is specified twice at different levels. In such a situation, getvalue attempts to find an exact match, and if that fails it returns the value with the most specific key.
It is the user's responsibility to avoid such cases by ensuring that the dictionary passed in does not contain the same value specified multiple times.
julia> vals = Dict(@varname(x) => [[1.0]], @varname(x[1]) => [2.0]);
julia> # Here, the `x[1]` key is not used because `x` is an exact match.
getvalue(vals, @varname(x))
1-element Vector{Vector{Float64}}:
[1.0]
julia> # Likewise, the `x` key is not used because `x[1]` is an exact match.
getvalue(vals, @varname(x[1]))
1-element Vector{Float64}:
2.0
julia> # No exact match, so the most specific key, i.e. `x[1]`, is used.
getvalue(vals, @varname(x[1][1]))
2.0Serializing VarNames
AbstractPPL.index_to_dict — Function
index_to_dict(::Integer)
index_to_dict(::AbstractVector{Int})
index_to_dict(::UnitRange)
index_to_dict(::StepRange)
index_to_dict(::Colon)
index_to_dict(::ConcretizedSlice{T, Base.OneTo{I}}) where {T, I}
index_to_dict(::Tuple)Convert an index i to a dictionary representation.
AbstractPPL.dict_to_index — Function
dict_to_index(dict)
dict_to_index(symbol_val, dict)Convert a dictionary representation of an index dict to an index.
Users can extend the functionality of dict_to_index (and hence VarName de/serialisation) by extending this method along with index_to_dict. Specifically, suppose you have a custom index type MyIndexType and you want to be able to de/serialise a VarName containing this index type. You should then implement the following two methods:
AbstractPPL.index_to_dict(i::MyModule.MyIndexType)should return a dictionary representation of the indexi. This dictionary must contain the key"type", and the corresponding value must be a string that uniquely identifies the index type. Generally, it makes sense to use the name of the type (perhaps prefixed with module qualifiers) as this value to avoid clashes. The remainder of the dictionary can have any structure you like.Suppose the value of
index_to_dict(i)["type"]is"MyModule.MyIndexType". You should then implement the corresponding methodAbstractPPL.dict_to_index(::Val{Symbol("MyModule.MyIndexType")}, dict), which should take the dictionary representation as the second argument and return the originalMyIndexTypeobject.
To see an example of this in action, you can look in the the AbstractPPL test suite, which contains a test for serialising OffsetArrays.
AbstractPPL.varname_to_string — Function
varname_to_string(vn::VarName)Convert a VarName as a string, via an intermediate dictionary. This differs from string(vn) in that concretised slices are faithfully represented (rather than being pretty-printed as colons).
For VarNames which index into an array, this function will only work if the indices can be serialised. This is true for all standard Julia index types, but if you are using custom index types, you will need to implement the index_to_dict and dict_to_index methods for those types. See the documentation of dict_to_index for instructions on how to do this.
julia> varname_to_string(@varname(x))
"{\"optic\":{\"type\":\"Iden\"},\"sym\":\"x\"}"
julia> varname_to_string(@varname(x.a))
"{\"optic\":{\"child\":{\"type\":\"Iden\"},\"field\":\"a\",\"type\":\"Property\"},\"sym\":\"x\"}"AbstractPPL.string_to_varname — Function
string_to_varname(str::AbstractString)Convert a string representation of a VarName back to a VarName. The string should have been generated by varname_to_string.