Skip to content

Commit 7c681fa

Browse files
committed
Normalise optics when constructing VarName; remove deprecated methods
1 parent a39791b commit 7c681fa

File tree

8 files changed

+148
-93
lines changed

8 files changed

+148
-93
lines changed

HISTORY.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
## 0.12.0
2+
3+
### VarName constructors
4+
5+
Removed the constructors `VarName(vn, optic)` (this wasn't deprecated, but was dangerous as it would silently discard the existing optic in `vn`), and `VarName(vn, ::Tuple)` (which was deprecated).
6+
7+
### Optic normalisation
8+
9+
In the inner constructor of a VarName, its optic is now normalised to ensure that the associativity of ComposedFunction is always the same, and that compositions with identity are removed.
10+
This helps to prevent subtle bugs where VarNames with semantically equal optics are not considered equal.
11+
12+
## 0.11.0
13+
14+
Added the `prefix(vn::VarName, vn_prefix::VarName)` and `unprefix(vn::VarName, vn_prefix::VarName)` functions.

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ uuid = "7a57a42e-76ec-4ea3-a279-07e840d6d9cf"
33
keywords = ["probablistic programming"]
44
license = "MIT"
55
desc = "Common interfaces for probabilistic programming"
6-
version = "0.11.0"
6+
version = "0.12.0"
77

88
[deps]
99
AbstractMCMC = "80f14c24-f653-4e6a-9b94-39d6b0f70001"

src/AbstractPPL.jl

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,5 @@ include("varname.jl")
2929
include("abstractmodeltrace.jl")
3030
include("abstractprobprog.jl")
3131
include("evaluate.jl")
32-
include("deprecations.jl")
3332

3433
end # module

src/deprecations.jl

Lines changed: 0 additions & 2 deletions
This file was deleted.

src/varname.jl

Lines changed: 76 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
using Accessors
2-
using Accessors: ComposedOptic, PropertyLens, IndexLens, DynamicIndexLens
2+
using Accessors: PropertyLens, IndexLens, DynamicIndexLens
33
using JSON: JSON
44

5-
const ALLOWED_OPTICS = Union{typeof(identity),PropertyLens,IndexLens,ComposedOptic}
5+
const ALLOWED_OPTICS = Union{typeof(identity),PropertyLens,IndexLens,ComposedFunction}
66

77
"""
88
VarName{sym}(optic=identity)
@@ -31,10 +31,11 @@ julia> @varname x[:, 1][1+1]
3131
x[:, 1][2]
3232
```
3333
"""
34-
struct VarName{sym,T}
34+
struct VarName{sym,T<:ALLOWED_OPTICS}
3535
optic::T
3636

3737
function VarName{sym}(optic=identity) where {sym}
38+
optic = normalise(optic)
3839
if !is_static_optic(typeof(optic))
3940
throw(
4041
ArgumentError(
@@ -53,42 +54,68 @@ Return `true` if `l` is one or a composition of `identity`, `PropertyLens`, and
5354
one or a composition of `DynamicIndexLens`; and undefined otherwise.
5455
"""
5556
is_static_optic(::Type{<:Union{typeof(identity),PropertyLens,IndexLens}}) = true
56-
function is_static_optic(::Type{ComposedOptic{LO,LI}}) where {LO,LI}
57+
function is_static_optic(::Type{ComposedFunction{LO,LI}}) where {LO,LI}
5758
return is_static_optic(LO) && is_static_optic(LI)
5859
end
5960
is_static_optic(::Type{<:DynamicIndexLens}) = false
6061

61-
# A bit of backwards compatibility.
62-
VarName{sym}(indexing::Tuple) where {sym} = VarName{sym}(tupleindex2optic(indexing))
63-
6462
"""
65-
VarName(vn::VarName, optic)
66-
VarName(vn::VarName, indexing::Tuple)
63+
normalise(optic)
6764
68-
Return a copy of `vn` with a new index `optic`/`indexing`.
65+
Enforce that compositions of optics are always nested in the same way, in that
66+
a ComposedFunction never has a ComposedFunction as its inner lens. Thus, for
67+
example,
6968
7069
```jldoctest; setup=:(using Accessors)
71-
julia> VarName(@varname(x[1][2:3]), Accessors.IndexLens((2,)))
72-
x[2]
70+
julia> op1 = ((@o _.c) ∘ (@o _.b)) ∘ (@o _.a)
71+
(@o _.a.b.c)
7372
74-
julia> VarName(@varname(x[1][2:3]), ((2,),))
75-
x[2]
73+
julia> op2 = (@o _.c) ∘ ((@o _.b) ∘ (@o _.a))
74+
(@o _.c) ∘ ((@o _.a.b))
7675
77-
julia> VarName(@varname(x[1][2:3]))
78-
x
76+
julia> op1 == op2
77+
false
78+
79+
julia> AbstractPPL.normalise(op1) == AbstractPPL.normalise(op2) == @o _.a.b.c
80+
true
7981
```
80-
"""
81-
VarName(vn::VarName, optic=identity) = VarName{getsym(vn)}(optic)
8282
83-
function VarName(vn::VarName, indexing::Tuple)
84-
return VarName{getsym(vn)}(tupleindex2optic(indexing))
85-
end
83+
This function also removes redundant `identity` optics from ComposedFunctions:
84+
85+
```jldoctest; setup=:(using Accessors)
86+
julia> op3 = ((@o _.b) ∘ identity) ∘ (@o _.a)
87+
(@o identity(_.a).b)
8688
87-
tupleindex2optic(indexing::Tuple{}) = identity
88-
tupleindex2optic(indexing::Tuple{<:Tuple}) = IndexLens(first(indexing)) # TODO: rest?
89-
function tupleindex2optic(indexing::Tuple)
90-
return IndexLens(first(indexing)) tupleindex2optic(indexing[2:end])
89+
julia> op4 = (@o _.b) ∘ (identity ∘ (@o _.a))
90+
(@o _.b) ∘ ((@o identity(_.a)))
91+
92+
julia> AbstractPPL.normalise(op3) == AbstractPPL.normalise(op4) == @o _.a.b
93+
true
94+
```
95+
"""
96+
function normalise(o::ComposedFunction{Outer,<:ComposedFunction}) where {Outer}
97+
# `o` is currently (outer ∘ (inner_outer ∘ inner_inner)).
98+
# We want to change this to:
99+
# o = (outer ∘ inner_outer) ∘ inner_inner
100+
inner_inner = o.inner.inner
101+
inner_outer = o.inner.outer
102+
# Recursively call normalise because inner_inner could itself be a
103+
# ComposedFunction
104+
return normalise((o.outer inner_outer) inner_inner)
105+
end
106+
function normalise(o::ComposedFunction{Outer,typeof(identity)} where {Outer})
107+
# strip outer identity
108+
return normalise(o.outer)
109+
end
110+
function normalise(o::ComposedFunction{typeof(identity),Inner} where {Inner})
111+
# strip inner identity
112+
return normalise(o.inner)
91113
end
114+
normalise(o::ComposedFunction) = normalise(o.outer) o.inner
115+
normalise(o::ALLOWED_OPTICS) = o
116+
# These two methods are needed to avoid method ambiguity.
117+
normalise(o::ComposedFunction{typeof(identity),<:ComposedFunction}) = normalise(o.inner)
118+
normalise(::ComposedFunction{typeof(identity),typeof(identity)}) = identity
92119

93120
"""
94121
getsym(vn::VarName)
@@ -105,7 +132,7 @@ julia> getsym(@varname(y))
105132
:y
106133
```
107134
"""
108-
getsym(vn::VarName{sym}) where {sym} = sym
135+
getsym(::VarName{sym}) where {sym} = sym
109136

110137
"""
111138
getoptic(vn::VarName)
@@ -154,15 +181,8 @@ function Accessors.set(obj, vn::VarName{sym}, value) where {sym}
154181
end
155182

156183
# Allow compositions with optic.
157-
function Base.:(optic::ALLOWED_OPTICS, vn::VarName{sym,<:ALLOWED_OPTICS}) where {sym}
158-
vn_optic = getoptic(vn)
159-
if vn_optic == identity
160-
return VarName{sym}(optic)
161-
elseif optic == identity
162-
return vn
163-
else
164-
return VarName{sym}(optic vn_optic)
165-
end
184+
function Base.:(optic::ALLOWED_OPTICS, vn::VarName{sym}) where {sym}
185+
return VarName{sym}(optic getoptic(vn))
166186
end
167187

168188
Base.hash(vn::VarName, h::UInt) = hash((getsym(vn), getoptic(vn)), h)
@@ -299,17 +319,17 @@ subsumes(::typeof(identity), ::typeof(identity)) = true
299319
subsumes(::typeof(identity), ::ALLOWED_OPTICS) = true
300320
subsumes(::ALLOWED_OPTICS, ::typeof(identity)) = false
301321

302-
function subsumes(t::ComposedOptic, u::ComposedOptic)
322+
function subsumes(t::ComposedFunction, u::ComposedFunction)
303323
return subsumes(t.outer, u.outer) && subsumes(t.inner, u.inner)
304324
end
305325

306326
# If `t` is still a composed lens, then there is no way it can subsume `u` since `u` is a
307327
# leaf of the "lens-tree".
308-
subsumes(t::ComposedOptic, u::PropertyLens) = false
328+
subsumes(t::ComposedFunction, u::PropertyLens) = false
309329
# Here we need to check if `u.inner` (i.e. the next lens to be applied from `u`) is
310330
# subsumed by `t`, since this would mean that the rest of the composition is also subsumed
311331
# by `t`.
312-
subsumes(t::PropertyLens, u::ComposedOptic) = subsumes(t, u.inner)
332+
subsumes(t::PropertyLens, u::ComposedFunction) = subsumes(t, u.inner)
313333

314334
# For `PropertyLens` either they have the same `name` and thus they are indeed the same.
315335
subsumes(t::PropertyLens{name}, u::PropertyLens{name}) where {name} = true
@@ -321,8 +341,8 @@ subsumes(t::PropertyLens, u::PropertyLens) = false
321341
# FIXME: Does not correctly handle cases such as `subsumes(x, x[:])`
322342
# (but neither did old implementation).
323343
function subsumes(
324-
t::Union{IndexLens,ComposedOptic{<:ALLOWED_OPTICS,<:IndexLens}},
325-
u::Union{IndexLens,ComposedOptic{<:ALLOWED_OPTICS,<:IndexLens}},
344+
t::Union{IndexLens,ComposedFunction{<:ALLOWED_OPTICS,<:IndexLens}},
345+
u::Union{IndexLens,ComposedFunction{<:ALLOWED_OPTICS,<:IndexLens}},
326346
)
327347
return subsumes_indices(t, u)
328348
end
@@ -415,7 +435,7 @@ The result is compatible with [`subsumes_indices`](@ref) for `Tuple` input.
415435
"""
416436
combine_indices(optic::ALLOWED_OPTICS) = (), optic
417437
combine_indices(optic::IndexLens) = (optic.indices,), nothing
418-
function combine_indices(optic::ComposedOptic{<:ALLOWED_OPTICS,<:IndexLens})
438+
function combine_indices(optic::ComposedFunction{<:ALLOWED_OPTICS,<:IndexLens})
419439
indices, next = combine_indices(optic.outer)
420440
return (optic.inner.indices, indices...), next
421441
end
@@ -505,9 +525,9 @@ concretize(I::DynamicIndexLens, x) = concretize(IndexLens(I.f(x)), x)
505525
function concretize(I::IndexLens, x)
506526
return IndexLens(reconcretize_index.(I.indices, to_indices(x, I.indices)))
507527
end
508-
function concretize(I::ComposedOptic, x)
528+
function concretize(I::ComposedFunction, x)
509529
x_inner = I.inner(x) # TODO: get view here
510-
return ComposedOptic(concretize(I.outer, x_inner), concretize(I.inner, x))
530+
return ComposedFunction(concretize(I.outer, x_inner), concretize(I.inner, x))
511531
end
512532

513533
"""
@@ -533,7 +553,7 @@ julia> # The underlying value is concretized, though:
533553
ConcretizedSlice(Base.OneTo(100))
534554
```
535555
"""
536-
concretize(vn::VarName, x) = VarName(vn, concretize(getoptic(vn), x))
556+
concretize(vn::VarName{sym}, x) where {sym} = VarName{sym}(concretize(getoptic(vn), x))
537557

538558
"""
539559
@varname(expr, concretize=false)
@@ -872,7 +892,7 @@ function optic_to_dict(::PropertyLens{sym}) where {sym}
872892
return Dict("type" => "property", "field" => String(sym))
873893
end
874894
optic_to_dict(i::IndexLens) = Dict("type" => "index", "indices" => index_to_dict(i.indices))
875-
function optic_to_dict(c::ComposedOptic)
895+
function optic_to_dict(c::ComposedFunction)
876896
return Dict(
877897
"type" => "composed",
878898
"outer" => optic_to_dict(c.outer),
@@ -1036,32 +1056,34 @@ ERROR: ArgumentError: optic_to_vn: could not convert optic `(@o _[1])` to a VarN
10361056
function optic_to_vn(::Accessors.PropertyLens{sym}) where {sym}
10371057
return VarName{sym}()
10381058
end
1039-
function optic_to_vn(o::Base.ComposedFunction{Outer,typeof(identity)}) where {Outer}
1040-
return optic_to_vn(o.outer)
1041-
end
10421059
function optic_to_vn(
10431060
o::Base.ComposedFunction{Outer,Accessors.PropertyLens{sym}}
10441061
) where {Outer,sym}
10451062
return VarName{sym}(o.outer)
10461063
end
1064+
optic_to_vn(o::Base.ComposedFunction) = optic_to_vn(normalise(o))
10471065
function optic_to_vn(@nospecialize(o))
10481066
msg = "optic_to_vn: could not convert optic `$o` to a VarName"
10491067
throw(ArgumentError(msg))
10501068
end
10511069

10521070
unprefix_optic(o, ::typeof(identity)) = o # Base case
10531071
function unprefix_optic(optic, optic_prefix)
1072+
# Technically `unprefix_optic` only receives optics that were part of
1073+
# VarNames, so the optics should already be normalised (in the inner
1074+
# constructor of the VarName). However I guess it doesn't hurt to do it
1075+
# again to be safe.
1076+
optic = normalise(optic)
1077+
optic_prefix = normalise(optic_prefix)
10541078
# strip one layer of the optic and check for equality
1055-
inner = _inner(_strip_identity(optic))
1056-
inner_prefix = _inner(_strip_identity(optic_prefix))
1079+
inner = _inner(optic)
1080+
inner_prefix = _inner(optic_prefix)
10571081
if inner != inner_prefix
10581082
msg = "could not remove prefix $(optic_prefix) from optic $(optic)"
10591083
throw(ArgumentError(msg))
10601084
end
10611085
# recurse
1062-
return unprefix_optic(
1063-
_outer(_strip_identity(optic)), _outer(_strip_identity(optic_prefix))
1064-
)
1086+
return unprefix_optic(_outer(optic), _outer(optic_prefix))
10651087
end
10661088

10671089
"""
@@ -1115,16 +1137,6 @@ y[1].x.a
11151137
function prefix(vn::VarName{sym_vn}, prefix::VarName{sym_prefix}) where {sym_vn,sym_prefix}
11161138
optic_vn = getoptic(vn)
11171139
optic_prefix = getoptic(prefix)
1118-
# Special case `identity` to avoid having ComposedFunctions with identity
1119-
if optic_vn == identity
1120-
new_inner_optic_vn = PropertyLens{sym_vn}()
1121-
else
1122-
new_inner_optic_vn = optic_vn PropertyLens{sym_vn}()
1123-
end
1124-
if optic_prefix == identity
1125-
new_optic_vn = new_inner_optic_vn
1126-
else
1127-
new_optic_vn = new_inner_optic_vn optic_prefix
1128-
end
1140+
new_optic_vn = optic_vn PropertyLens{sym_vn}() optic_prefix
11291141
return VarName{sym_prefix}(new_optic_vn)
11301142
end

test/deprecations.jl

Lines changed: 0 additions & 4 deletions
This file was deleted.

test/runtests.jl

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,3 @@
1-
# Activate test environment on older Julia versions
2-
if VERSION < v"1.2"
3-
using Pkg: Pkg
4-
Pkg.activate(@__DIR__)
5-
Pkg.develop(Pkg.PackageSpec(; path=dirname(@__DIR__)))
6-
Pkg.instantiate()
7-
end
8-
91
using AbstractPPL
102
using Documenter
113
using Test
@@ -14,7 +6,6 @@ const GROUP = get(ENV, "GROUP", "All")
146

157
@testset "AbstractPPL.jl" begin
168
if GROUP == "All" || GROUP == "Tests"
17-
include("deprecations.jl")
189
include("varname.jl")
1910
include("abstractprobprog.jl")
2011
end

0 commit comments

Comments
 (0)