So this internal :new expression seems to be working again. I’m wondering if it’s possible to unsafely skip inner constructors’ validation to instantiate tweaks to existing instances with setting syntax like Accessors.jl.
julia> macro new(call::Expr)
if call.head != :call
return call
else
Expr(:new, call.args...)
end
end
@new (macro with 1 method)
julia> 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
julia> @time y = EvenInt(2)
1.002108 seconds (4 allocations: 112 bytes)
EvenInt(2)
julia> @time @new(EvenInt(1)) # skips validation like mutation
0.000001 seconds
EvenInt(1)
julia> using Accessors
julia> @time @reset y.x = 4 # goes through inner constructor
1.034079 seconds (5.05 k allocations: 260.352 KiB, 2.56% compilation time: 100% of which was recompilation)
EvenInt(4)
Yes, the way you do this is by overloading ConstructionBase.constructorof:
using Accessors, ConstructionBase
macro new(call::Expr)
if call.head != :call
esc(return call)
else
esc(Expr(:new, call.args...)) # Note this needs to be esc-d!
end
end
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
ConstructionBase.constructorof(::Type{EvenInt}) = x -> @new EvenInt(convert(Int, x))
Now, whether or not this is a good idea is of course up for discussion. The authors of Accessors / ConstructionBase made a conscious choice to work in terms of properties and constructors rather than using @new because people put restrictions in constructors for a good reason.
That said, there are of course times where you want to be able to bypass this, so it’s good that there is a way (though I would like if there was a clean way to do this on a per-call basis, rather than globally overriding the behaviour for all instances of EvenInt).
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
The call chain for setting a property, as in y = @set x.a = 123, goes like this by default:
first setproperties(x, (;a=123))
then constructorof(typeof(x))(123)
then the constructor.
Depending on the intended semantics of your type, you may want to overload either setproperties or constructorof – both are defined in ConstructionBase.jl.