Skip to content

Commit 68ad707

Browse files
Conversion of VarName to/from string (#100)
* Fix typo in comment * Conversion of VarName to/from string * Add one more test * More thorough serialisation * Add doctests * Add API docs * Add warning to docstring Co-authored-by: Tor Erlend Fjelde <[email protected]> * Add alternate implementation with StructTypes * Reduce calls to Meta.parse() It's only called for ConcretizedSlice now, which could potentially be removed too. * Restrict allowed ranges for ConcretizedSlice * Fix name of wrapper type * More tests * Remove unneeded extra method for ConcretizedSlice * Add StepRange support * Support arrays of integers as indices * Simplify implementation even more * Bump to 0.9.0 * Clean up old code, add docs * Allow de/serialisation methods to be extended * Update docs * Name functions more consistently --------- Co-authored-by: Tor Erlend Fjelde <[email protected]>
1 parent a77e247 commit 68ad707

File tree

5 files changed

+225
-3
lines changed

5 files changed

+225
-3
lines changed

Diff for: Project.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf"
33
keywords = ["probablistic programming"]
44
license = "MIT"
55
desc = "Common interfaces for probabilistic programming"
6-
version = "0.8.4"
6+
version = "0.9.0"
77

88
[deps]
99
AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001"
1010
Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697"
1111
DensityInterface = "b429d917-457f-4dbc-8f4c-0cc954292b1d"
12+
JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6"
1213
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
1314

1415
[compat]

Diff for: docs/src/api.md

+9
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,15 @@ vsym
1414
@vsym
1515
```
1616

17+
## VarName serialisation
18+
19+
```@docs
20+
index_to_dict
21+
dict_to_index
22+
varname_to_string
23+
string_to_varname
24+
```
25+
1726
## Abstract model functions
1827

1928
```@docs

Diff for: src/AbstractPPL.jl

+5-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@ export VarName,
1010
varname,
1111
vsym,
1212
@varname,
13-
@vsym
13+
@vsym,
14+
index_to_dict,
15+
dict_to_index,
16+
varname_to_string,
17+
string_to_varname
1418

1519

1620
# Abstract model functions

Diff for: src/varname.jl

+146-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using Accessors
22
using Accessors: ComposedOptic, PropertyLens, IndexLens, DynamicIndexLens
3+
using JSON: JSON
34

45
const ALLOWED_OPTICS = Union{typeof(identity),PropertyLens,IndexLens,ComposedOptic}
56

@@ -302,7 +303,7 @@ subsumes(t::ComposedOptic, u::ComposedOptic) =
302303
# If `t` is still a composed lens, then there is no way it can subsume `u` since `u` is a
303304
# leaf of the "lens-tree".
304305
subsumes(t::ComposedOptic, u::PropertyLens) = false
305-
# Here we need to check if `u.outer` (i.e. the next lens to be applied from `u`) is
306+
# Here we need to check if `u.inner` (i.e. the next lens to be applied from `u`) is
306307
# subsumed by `t`, since this would mean that the rest of the composition is also subsumed
307308
# by `t`.
308309
subsumes(t::PropertyLens, u::ComposedOptic) = subsumes(t, u.inner)
@@ -752,3 +753,147 @@ function vsym(expr::Expr)
752753
error("Malformed variable name `$(expr)`!")
753754
end
754755
end
756+
757+
# String constants for each index type that we support serialisation /
758+
# deserialisation of
759+
const _BASE_INTEGER_TYPE = "Base.Integer"
760+
const _BASE_VECTOR_TYPE = "Base.Vector"
761+
const _BASE_UNITRANGE_TYPE = "Base.UnitRange"
762+
const _BASE_STEPRANGE_TYPE = "Base.StepRange"
763+
const _BASE_ONETO_TYPE = "Base.OneTo"
764+
const _BASE_COLON_TYPE = "Base.Colon"
765+
const _CONCRETIZED_SLICE_TYPE = "AbstractPPL.ConcretizedSlice"
766+
const _BASE_TUPLE_TYPE = "Base.Tuple"
767+
768+
"""
769+
index_to_dict(::Integer)
770+
index_to_dict(::AbstractVector{Int})
771+
index_to_dict(::UnitRange)
772+
index_to_dict(::StepRange)
773+
index_to_dict(::Colon)
774+
index_to_dict(::ConcretizedSlice{T, Base.OneTo{I}}) where {T, I}
775+
index_to_dict(::Tuple)
776+
777+
Convert an index `i` to a dictionary representation.
778+
"""
779+
index_to_dict(i::Integer) = Dict("type" => _BASE_INTEGER_TYPE, "value" => i)
780+
index_to_dict(v::Vector{Int}) = Dict("type" => _BASE_VECTOR_TYPE, "values" => v)
781+
index_to_dict(r::UnitRange) = Dict("type" => _BASE_UNITRANGE_TYPE, "start" => r.start, "stop" => r.stop)
782+
index_to_dict(r::StepRange) = Dict("type" => _BASE_STEPRANGE_TYPE, "start" => r.start, "stop" => r.stop, "step" => r.step)
783+
index_to_dict(r::Base.OneTo{I}) where {I} = Dict("type" => _BASE_ONETO_TYPE, "stop" => r.stop)
784+
index_to_dict(::Colon) = Dict("type" => _BASE_COLON_TYPE)
785+
index_to_dict(s::ConcretizedSlice{T,R}) where {T,R} = Dict("type" => _CONCRETIZED_SLICE_TYPE, "range" => index_to_dict(s.range))
786+
index_to_dict(t::Tuple) = Dict("type" => _BASE_TUPLE_TYPE, "values" => map(index_to_dict, t))
787+
788+
"""
789+
dict_to_index(dict)
790+
dict_to_index(symbol_val, dict)
791+
792+
Convert a dictionary representation of an index `dict` to an index.
793+
794+
Users can extend the functionality of `dict_to_index` (and hence `VarName`
795+
de/serialisation) by extending this method along with [`index_to_dict`](@ref).
796+
Specifically, suppose you have a custom index type `MyIndexType` and you want
797+
to be able to de/serialise a `VarName` containing this index type. You should
798+
then implement the following two methods:
799+
800+
1. `AbstractPPL.index_to_dict(i::MyModule.MyIndexType)` should return a
801+
dictionary representation of the index `i`. This dictionary must contain the
802+
key `"type"`, and the corresponding value must be a string that uniquely
803+
identifies the index type. Generally, it makes sense to use the name of the
804+
type (perhaps prefixed with module qualifiers) as this value to avoid
805+
clashes. The remainder of the dictionary can have any structure you like.
806+
807+
2. Suppose the value of `index_to_dict(i)["type"]` is `"MyModule.MyIndexType"`.
808+
You should then implement the corresponding method
809+
`AbstractPPL.dict_to_index(::Val{Symbol("MyModule.MyIndexType")}, dict)`,
810+
which should take the dictionary representation as the second argument and
811+
return the original `MyIndexType` object.
812+
813+
To see an example of this in action, you can look in the the AbstractPPL test
814+
suite, which contains a test for serialising OffsetArrays.
815+
"""
816+
function dict_to_index(dict)
817+
t = dict["type"]
818+
if t == _BASE_INTEGER_TYPE
819+
return dict["value"]
820+
elseif t == _BASE_VECTOR_TYPE
821+
return collect(Int, dict["values"])
822+
elseif t == _BASE_UNITRANGE_TYPE
823+
return dict["start"]:dict["stop"]
824+
elseif t == _BASE_STEPRANGE_TYPE
825+
return dict["start"]:dict["step"]:dict["stop"]
826+
elseif t == _BASE_ONETO_TYPE
827+
return Base.OneTo(dict["stop"])
828+
elseif t == _BASE_COLON_TYPE
829+
return Colon()
830+
elseif t == _CONCRETIZED_SLICE_TYPE
831+
return ConcretizedSlice(Base.Slice(dict_to_index(dict["range"])))
832+
elseif t == _BASE_TUPLE_TYPE
833+
return tuple(map(dict_to_index, dict["values"])...)
834+
else
835+
# Will error if the method is not defined, but this hook allows users
836+
# to extend this function
837+
return dict_to_index(Val(Symbol(t)), dict)
838+
end
839+
end
840+
841+
optic_to_dict(::typeof(identity)) = Dict("type" => "identity")
842+
optic_to_dict(::PropertyLens{sym}) where {sym} = Dict("type" => "property", "field" => String(sym))
843+
optic_to_dict(i::IndexLens) = Dict("type" => "index", "indices" => index_to_dict(i.indices))
844+
optic_to_dict(c::ComposedOptic) = Dict("type" => "composed", "outer" => optic_to_dict(c.outer), "inner" => optic_to_dict(c.inner))
845+
846+
function dict_to_optic(dict)
847+
if dict["type"] == "identity"
848+
return identity
849+
elseif dict["type"] == "index"
850+
return IndexLens(dict_to_index(dict["indices"]))
851+
elseif dict["type"] == "property"
852+
return PropertyLens{Symbol(dict["field"])}()
853+
elseif dict["type"] == "composed"
854+
return dict_to_optic(dict["outer"]) dict_to_optic(dict["inner"])
855+
else
856+
error("Unknown optic type: $(dict["type"])")
857+
end
858+
end
859+
860+
varname_to_dict(vn::VarName) = Dict("sym" => getsym(vn), "optic" => optic_to_dict(getoptic(vn)))
861+
862+
dict_to_varname(dict::Dict{<:AbstractString, Any}) = VarName{Symbol(dict["sym"])}(dict_to_optic(dict["optic"]))
863+
864+
"""
865+
varname_to_string(vn::VarName)
866+
867+
Convert a `VarName` as a string, via an intermediate dictionary. This differs
868+
from `string(vn)` in that concretised slices are faithfully represented (rather
869+
than being pretty-printed as colons).
870+
871+
For `VarName`s which index into an array, this function will only work if the
872+
indices can be serialised. This is true for all standard Julia index types, but
873+
if you are using custom index types, you will need to implement the
874+
`index_to_dict` and `dict_to_index` methods for those types. See the
875+
documentation of [`dict_to_index`](@ref) for instructions on how to do this.
876+
877+
```jldoctest
878+
julia> varname_to_string(@varname(x))
879+
"{\\"optic\\":{\\"type\\":\\"identity\\"},\\"sym\\":\\"x\\"}"
880+
881+
julia> varname_to_string(@varname(x.a))
882+
"{\\"optic\\":{\\"field\\":\\"a\\",\\"type\\":\\"property\\"},\\"sym\\":\\"x\\"}"
883+
884+
julia> y = ones(2); varname_to_string(@varname(y[:]))
885+
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"type\\":\\"Base.Colon\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
886+
887+
julia> y = ones(2); varname_to_string(@varname(y[:], true))
888+
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"range\\":{\\"stop\\":2,\\"type\\":\\"Base.OneTo\\"},\\"type\\":\\"AbstractPPL.ConcretizedSlice\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
889+
```
890+
"""
891+
varname_to_string(vn::VarName) = JSON.json(varname_to_dict(vn))
892+
893+
"""
894+
string_to_varname(str::AbstractString)
895+
896+
Convert a string representation of a `VarName` back to a `VarName`. The string
897+
should have been generated by `varname_to_string`.
898+
"""
899+
string_to_varname(str::AbstractString) = dict_to_varname(JSON.parse(str))

Diff for: test/varname.jl

+63
Original file line numberDiff line numberDiff line change
@@ -137,4 +137,67 @@ end
137137
@inferred get(c, @varname(b.a[1]))
138138
@inferred Accessors.set(c, @varname(b.a[1]), 10)
139139
end
140+
141+
@testset "de/serialisation of VarNames" begin
142+
y = ones(10)
143+
z = ones(5, 2)
144+
vns = [
145+
@varname(x),
146+
@varname(ä),
147+
@varname(x.a),
148+
@varname(x.a.b),
149+
@varname(var"x.a"),
150+
@varname(x[1]),
151+
@varname(var"x[1]"),
152+
@varname(x[1:10]),
153+
@varname(x[1:3:10]),
154+
@varname(x[1, 2]),
155+
@varname(x[1, 2:5]),
156+
@varname(x[:]),
157+
@varname(x.a[1]),
158+
@varname(x.a[1:10]),
159+
@varname(x[1].a),
160+
@varname(y[:]),
161+
@varname(y[begin:end]),
162+
@varname(y[end]),
163+
@varname(y[:], false),
164+
@varname(y[:], true),
165+
@varname(z[:], false),
166+
@varname(z[:], true),
167+
@varname(z[:][:], false),
168+
@varname(z[:][:], true),
169+
@varname(z[:,:], false),
170+
@varname(z[:,:], true),
171+
@varname(z[2:5,:], false),
172+
@varname(z[2:5,:], true),
173+
]
174+
for vn in vns
175+
@test string_to_varname(varname_to_string(vn)) == vn
176+
end
177+
178+
# For this VarName, the {de,}serialisation works correctly but we must
179+
# test in a different way because equality comparison of structs with
180+
# vector fields (such as Accessors.IndexLens) compares the memory
181+
# addresses rather than the contents (thus vn_vec == vn_vec2 returns
182+
# false).
183+
vn_vec = @varname(x[[1, 2, 5, 6]])
184+
vn_vec2 = string_to_varname(varname_to_string(vn_vec))
185+
@test hash(vn_vec) == hash(vn_vec2)
186+
end
187+
188+
@testset "de/serialisation of VarNames with custom index types" begin
189+
using OffsetArrays: OffsetArrays, Origin
190+
weird = Origin(4)(ones(10))
191+
vn = @varname(weird[:], true)
192+
193+
# This won't work as we don't yet know how to handle OffsetArray
194+
@test_throws MethodError varname_to_string(vn)
195+
196+
# Now define the relevant methods
197+
AbstractPPL.index_to_dict(o::OffsetArrays.IdOffsetRange{I, R}) where {I,R} = Dict("type" => "OffsetArrays.OffsetArray", "parent" => AbstractPPL.index_to_dict(o.parent), "offset" => o.offset)
198+
AbstractPPL.dict_to_index(::Val{Symbol("OffsetArrays.OffsetArray")}, d) = OffsetArrays.IdOffsetRange(AbstractPPL.dict_to_index(d["parent"]), d["offset"])
199+
200+
# Serialisation should now work
201+
@test string_to_varname(varname_to_string(vn)) == vn
202+
end
140203
end

0 commit comments

Comments
 (0)