Looks like this is actually not so hard to do by just defining a custom lens:
using Accessors: IndexLens, PropertyLens, ComposedOptic, setmacro, opticmacro, modifymacro
struct FieldLens{L}
inner::L
end
(l::FieldLens)(o) = l.inner(o)
function Accessors.set(o, l::FieldLens{<:ComposedOptic}, val)
o_inner = l.inner.inner(o)
set(o_inner, FieldLens(l.inner.outer), val)
end
function Accessors.set(o, l::FieldLens{PropertyLens{prop}}, val) where {prop}
setfield(o, val, Val(prop))
end
@generated function setfield(obj::T, val, ::Val{name}) where {T, name}
fields = fieldnames(T)
name ∈ fields || error("$(repr(name)) is not a field of $T, expected one of ", fields)
Expr(:new, T, (name == field ? :val : :(getfield(obj, $(QuoteNode(field)))) for field ∈ fields)...)
end
macro setfield(ex)
setmacro(FieldLens, ex, overwrite=false)
end
macro resetfield(ex)
setmacro(FieldLens, ex, overwrite=true)
end
then define our type:
struct EvenInt
x::Int
function EvenInt(x::Int)
iseven(x) || throw(ArgumentError("Must be even."))
sleep(1) # pretend this is useful processing
new(x)
end
end
Regular @reset
is still safe and slow:
julia> @time y = EvenInt(2)
1.002596 seconds (4 allocations: 112 bytes)
EvenInt(2)
julia> @reset y.x = 5
ERROR: ArgumentError: Must be even.
julia> @time @reset y.x = 4
1.002588 seconds (5 allocations: 128 bytes)
EvenInt(4)
But we now also have @resetfield
which is unsafe and fast:
julia> @resetfield y.x = 5
EvenInt(5)
julia> @time @resetfield y.x = 4
0.000005 seconds (1 allocation: 16 bytes)
EvenInt(4)