Skip to content

Commit

Permalink
Allow de/serialisation methods to be extended
Browse files Browse the repository at this point in the history
  • Loading branch information
penelopeysm committed Sep 30, 2024
1 parent fcd81b0 commit 34d229c
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 28 deletions.
7 changes: 7 additions & 0 deletions docs/src/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ subsumedby
vsym
@varname
@vsym
```

## VarName serialisation

```@docs
index_to_dict
dict_to_index
vn_to_string
vn_from_string
```
Expand Down
10 changes: 5 additions & 5 deletions src/AbstractPPL.jl
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,13 @@ export VarName,
subsumes,
subsumedby,
varname,
vn_to_string,
vn_from_string,
vn_to_string2,
vn_from_string2,
vsym,
@varname,
@vsym
@vsym,
index_to_dict,
dict_to_index,
vn_to_string,
vn_from_string


# Abstract model functions
Expand Down
105 changes: 83 additions & 22 deletions src/varname.jl
Original file line number Diff line number Diff line change
Expand Up @@ -754,32 +754,87 @@ function vsym(expr::Expr)
end
end

index_to_dict(i::Integer) = Dict("type" => "integer", "value" => i)
index_to_dict(v::AbstractVector{Int}) = Dict("type" => "vector", "values" => v)
index_to_dict(r::UnitRange) = Dict("type" => "unitrange", "start" => r.start, "stop" => r.stop)
index_to_dict(r::StepRange) = Dict("type" => "steprange", "start" => r.start, "stop" => r.stop, "step" => r.step)
index_to_dict(::Colon) = Dict("type" => "colon")
index_to_dict(s::ConcretizedSlice{T,Base.OneTo{I}}) where {T,I} = Dict("type" => "concretized_slice", "oneto" => s.range.stop)
index_to_dict(::ConcretizedSlice{T,R}) where {T,R} = error("ConcretizedSlice with range type $(R) not supported")
index_to_dict(t::Tuple) = Dict("type" => "tuple", "values" => map(index_to_dict, t))

# String constants for each index type that we support serialisation /
# deserialisation of
const _BASE_INTEGER_TYPE = "Base.Integer"
const _BASE_VECTOR_TYPE = "Base.Vector"
const _BASE_UNITRANGE_TYPE = "Base.UnitRange"
const _BASE_STEPRANGE_TYPE = "Base.StepRange"
const _BASE_ONETO_TYPE = "Base.OneTo"
const _BASE_COLON_TYPE = "Base.Colon"
const _CONCRETIZED_SLICE_TYPE = "AbstractPPL.ConcretizedSlice"
const _BASE_TUPLE_TYPE = "Base.Tuple"

"""
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.
"""
index_to_dict(i::Integer) = Dict("type" => _BASE_INTEGER_TYPE, "value" => i)
index_to_dict(v::Vector{Int}) = Dict("type" => _BASE_VECTOR_TYPE, "values" => v)
index_to_dict(r::UnitRange) = Dict("type" => _BASE_UNITRANGE_TYPE, "start" => r.start, "stop" => r.stop)
index_to_dict(r::StepRange) = Dict("type" => _BASE_STEPRANGE_TYPE, "start" => r.start, "stop" => r.stop, "step" => r.step)
index_to_dict(r::Base.OneTo{I}) where {I} = Dict("type" => _BASE_ONETO_TYPE, "stop" => r.stop)
index_to_dict(::Colon) = Dict("type" => _BASE_COLON_TYPE)
index_to_dict(s::ConcretizedSlice{T,R}) where {T,R} = Dict("type" => _CONCRETIZED_SLICE_TYPE, "range" => index_to_dict(s.range))
index_to_dict(t::Tuple) = Dict("type" => _BASE_TUPLE_TYPE, "values" => map(index_to_dict, t))

"""
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`](@ref).
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:
1. `AbstractPPL.index_to_dict(i::MyIndexType)` should return a dictionary
representation of the index `i`. 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.
2. Suppose the value of `index_to_dict(i)["type"]` is "MyModule.MyIndexType".
You should then implement the corresponding method
`AbstractPPL.dict_to_index(::Val{:MyModule.MyIndexType}, dict)`, which
should take the dictionary representation as the second argument and return
the original `MyIndexType` object.
To see an example of this in action, you can look in the the AbstractPPL test
suite, which contains a test for serialising OffsetArrays.
"""
function dict_to_index(dict)
if dict["type"] == "integer"
t = dict["type"]
if t == _BASE_INTEGER_TYPE
return dict["value"]
elseif dict["type"] == "vector"
elseif t == _BASE_VECTOR_TYPE
return collect(Int, dict["values"])
elseif dict["type"] == "unitrange"
elseif t == _BASE_UNITRANGE_TYPE
return dict["start"]:dict["stop"]
elseif dict["type"] == "steprange"
elseif t == _BASE_STEPRANGE_TYPE
return dict["start"]:dict["step"]:dict["stop"]
elseif dict["type"] == "colon"
elseif t == _BASE_ONETO_TYPE
return Base.OneTo(dict["stop"])
elseif t == _BASE_COLON_TYPE
return Colon()
elseif dict["type"] == "concretized_slice"
return ConcretizedSlice(Base.Slice(Base.OneTo(dict["oneto"])))
elseif dict["type"] == "tuple"
elseif t == _CONCRETIZED_SLICE_TYPE
return ConcretizedSlice(Base.Slice(dict_to_index(dict["range"])))
elseif t == _BASE_TUPLE_TYPE
return tuple(map(dict_to_index, dict["values"])...)
else
error("Unknown index type: $(dict["type"])")
# Will error if the method is not defined, but this hook allows users
# to extend this function
return dict_to_index(Val(Symbol(t)), dict)
end
end

Expand Down Expand Up @@ -813,6 +868,12 @@ 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 `VarName`s 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`](@ref) for instructions on how to do this.
```jldoctest
julia> vn_to_string(@varname(x))
"{\\"optic\\":{\\"type\\":\\"identity\\"},\\"sym\\":\\"x\\"}"
Expand All @@ -821,18 +882,18 @@ julia> vn_to_string(@varname(x.a))
"{\\"optic\\":{\\"field\\":\\"a\\",\\"type\\":\\"property\\"},\\"sym\\":\\"x\\"}"
julia> y = ones(2); vn_to_string(@varname(y[:]))
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"type\\":\\"colon\\"}],\\"type\\":\\"tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"type\\":\\"Base.Colon\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
julia> y = ones(2); vn_to_string(@varname(y[:], true))
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"oneto\\":2,\\"type\\":\\"concretized_slice\\"}],\\"type\\":\\"tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
"{\\"optic\\":{\\"indices\\":{\\"values\\":[{\\"range\\":{\\"stop\\":2,\\"type\\":\\"Base.OneTo\\"},\\"type\\":\\"AbstractPPL.ConcretizedSlice\\"}],\\"type\\":\\"Base.Tuple\\"},\\"type\\":\\"index\\"},\\"sym\\":\\"y\\"}"
```
"""
vn_to_string(vn::VarName) = JSON.json(vn_to_dict(vn))

"""
vn_from_string(str)
vn_from_string(str::AbstractString)
Convert a string representation of a `VarName` back to a `VarName`. The string
should have been generated by `vn_to_string`.
"""
vn_from_string(str) = dict_to_vn(JSON.parse(str))
vn_from_string(str::AbstractString) = dict_to_vn(JSON.parse(str))
18 changes: 17 additions & 1 deletion test/varname.jl
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ end
@inferred Accessors.set(c, @varname(b.a[1]), 10)
end

@testset "roundtrip conversion to/from string" begin
@testset "de/serialisation of VarNames" begin
y = ones(10)
z = ones(5, 2)
vns = [
Expand Down Expand Up @@ -184,4 +184,20 @@ end
vn_vec2 = vn_from_string(vn_to_string(vn_vec))
@test hash(vn_vec) == hash(vn_vec2)
end

@testset "de/serialisation of VarNames with custom index types" begin
using OffsetArrays: OffsetArrays, Origin
weird = Origin(4)(ones(10))
vn = @varname(weird[:], true)

# This won't work as we don't yet know how to handle OffsetArray
@test_throws MethodError vn_to_string(vn)

# Now define the relevant methods
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)
AbstractPPL.dict_to_index(::Val{Symbol("OffsetArrays.OffsetArray")}, d) = OffsetArrays.IdOffsetRange(AbstractPPL.dict_to_index(d["parent"]), d["offset"])

# Serialisation should now work
@test vn_from_string(vn_to_string(vn)) == vn
end
end

0 comments on commit 34d229c

Please sign in to comment.