Skip to content

Commit 34d229c

Browse files
committed
Allow de/serialisation methods to be extended
1 parent fcd81b0 commit 34d229c

File tree

4 files changed

+112
-28
lines changed

4 files changed

+112
-28
lines changed

docs/src/api.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,13 @@ subsumedby
1212
vsym
1313
@varname
1414
@vsym
15+
```
16+
17+
## VarName serialisation
18+
19+
```@docs
20+
index_to_dict
21+
dict_to_index
1522
vn_to_string
1623
vn_from_string
1724
```

src/AbstractPPL.jl

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ export VarName,
88
subsumes,
99
subsumedby,
1010
varname,
11-
vn_to_string,
12-
vn_from_string,
13-
vn_to_string2,
14-
vn_from_string2,
1511
vsym,
1612
@varname,
17-
@vsym
13+
@vsym,
14+
index_to_dict,
15+
dict_to_index,
16+
vn_to_string,
17+
vn_from_string
1818

1919

2020
# Abstract model functions

src/varname.jl

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -754,32 +754,87 @@ function vsym(expr::Expr)
754754
end
755755
end
756756

757-
index_to_dict(i::Integer) = Dict("type" => "integer", "value" => i)
758-
index_to_dict(v::AbstractVector{Int}) = Dict("type" => "vector", "values" => v)
759-
index_to_dict(r::UnitRange) = Dict("type" => "unitrange", "start" => r.start, "stop" => r.stop)
760-
index_to_dict(r::StepRange) = Dict("type" => "steprange", "start" => r.start, "stop" => r.stop, "step" => r.step)
761-
index_to_dict(::Colon) = Dict("type" => "colon")
762-
index_to_dict(s::ConcretizedSlice{T,Base.OneTo{I}}) where {T,I} = Dict("type" => "concretized_slice", "oneto" => s.range.stop)
763-
index_to_dict(::ConcretizedSlice{T,R}) where {T,R} = error("ConcretizedSlice with range type $(R) not supported")
764-
index_to_dict(t::Tuple) = Dict("type" => "tuple", "values" => map(index_to_dict, t))
765-
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::MyIndexType)` should return a dictionary
801+
representation of the index `i`. This dictionary must contain the key
802+
`"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{:MyModule.MyIndexType}, dict)`, which
810+
should take the dictionary representation as the second argument and return
811+
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+
"""
766816
function dict_to_index(dict)
767-
if dict["type"] == "integer"
817+
t = dict["type"]
818+
if t == _BASE_INTEGER_TYPE
768819
return dict["value"]
769-
elseif dict["type"] == "vector"
820+
elseif t == _BASE_VECTOR_TYPE
770821
return collect(Int, dict["values"])
771-
elseif dict["type"] == "unitrange"
822+
elseif t == _BASE_UNITRANGE_TYPE
772823
return dict["start"]:dict["stop"]
773-
elseif dict["type"] == "steprange"
824+
elseif t == _BASE_STEPRANGE_TYPE
774825
return dict["start"]:dict["step"]:dict["stop"]
775-
elseif dict["type"] == "colon"
826+
elseif t == _BASE_ONETO_TYPE
827+
return Base.OneTo(dict["stop"])
828+
elseif t == _BASE_COLON_TYPE
776829
return Colon()
777-
elseif dict["type"] == "concretized_slice"
778-
return ConcretizedSlice(Base.Slice(Base.OneTo(dict["oneto"])))
779-
elseif dict["type"] == "tuple"
830+
elseif t == _CONCRETIZED_SLICE_TYPE
831+
return ConcretizedSlice(Base.Slice(dict_to_index(dict["range"])))
832+
elseif t == _BASE_TUPLE_TYPE
780833
return tuple(map(dict_to_index, dict["values"])...)
781834
else
782-
error("Unknown index type: $(dict["type"])")
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)
783838
end
784839
end
785840

@@ -813,6 +868,12 @@ Convert a `VarName` as a string, via an intermediate dictionary. This differs
813868
from `string(vn)` in that concretised slices are faithfully represented (rather
814869
than being pretty-printed as colons).
815870
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+
816877
```jldoctest
817878
julia> vn_to_string(@varname(x))
818879
"{\\"optic\\":{\\"type\\":\\"identity\\"},\\"sym\\":\\"x\\"}"
@@ -821,18 +882,18 @@ julia> vn_to_string(@varname(x.a))
821882
"{\\"optic\\":{\\"field\\":\\"a\\",\\"type\\":\\"property\\"},\\"sym\\":\\"x\\"}"
822883
823884
julia> y = ones(2); vn_to_string(@varname(y[:]))
824-
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"type\\":\\"colon\\"}],\\"type\\":\\"tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
885+
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"type\\":\\"Base.Colon\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
825886
826887
julia> y = ones(2); vn_to_string(@varname(y[:], true))
827-
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"oneto\\":2,\\"type\\":\\"concretized_slice\\"}],\\"type\\":\\"tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
888+
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"range\\":{\\"stop\\":2,\\"type\\":\\"Base.OneTo\\"},\\"type\\":\\"AbstractPPL.ConcretizedSlice\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
828889
```
829890
"""
830891
vn_to_string(vn::VarName) = JSON.json(vn_to_dict(vn))
831892

832893
"""
833-
vn_from_string(str)
894+
vn_from_string(str::AbstractString)
834895
835896
Convert a string representation of a `VarName` back to a `VarName`. The string
836897
should have been generated by `vn_to_string`.
837898
"""
838-
vn_from_string(str) = dict_to_vn(JSON.parse(str))
899+
vn_from_string(str::AbstractString) = dict_to_vn(JSON.parse(str))

test/varname.jl

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ end
138138
@inferred Accessors.set(c, @varname(b.a[1]), 10)
139139
end
140140

141-
@testset "roundtrip conversion to/from string" begin
141+
@testset "de/serialisation of VarNames" begin
142142
y = ones(10)
143143
z = ones(5, 2)
144144
vns = [
@@ -184,4 +184,20 @@ end
184184
vn_vec2 = vn_from_string(vn_to_string(vn_vec))
185185
@test hash(vn_vec) == hash(vn_vec2)
186186
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 vn_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 vn_from_string(vn_to_string(vn)) == vn
202+
end
187203
end

0 commit comments

Comments
 (0)