I have defined a struct with an inner constructor that enforces some invariants,
similar to the example in the Documentation
julia> struct OrderedPair
x::Real
y::Real
OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
end
and was wondering if there is a way to bypass the inner constructor and call new directly when I am absolutely sure that the invariants are satisfied. E.g. to define copy I can do the following
One way would be just to add another inner constructor.
struct OrderedPair
x::Real
y::Real
OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
OrderedPair(op::OrderedPair) = new(copy(op.x), copy(op.y))
end
However, this generally won’t do what you want because since this is immutable and most Real types are also immutable, it doesn’t actually make a copy but just returns the same one.
For example, for all (?) the numeric types in Base, b === copy(b).
(Also, if performance is at all a consideration this really should be a parametric type)
This was just an example (in fact the one from the docs) to demonstrate what I want to do. In the actual use case the fields of the structs are arrays (hence mutable) and I am using parametric types. But adding a second inner constructor gave me an idea how to do it:
struct OrderedPair
x::Real
y::Real
OrderedPair(x, y, safe::Val{True}) = new(op.x, op.y)
OrderedPair(x, y, safe::Val{False}) = x > y ? error("out of order") : new(x,y)
OrderdPair(x, y) = OrderedPair(x, y, Val(false)) # could probably also have this as an outer constructor
end
I’ve started to define an internal constructor that is “unsafe” any time I have a new type defined.
struct OrderedPair{T <: Real}
x::T
y::T
global unsafe_oredered_pair(x::T, y::T) = new{T}(x, y)
end
function OrderedPair(x::T, y::T) where {T}
x > y && error("out of order")
unsafe_oredered_pair(x, y)
end
OrderedPair(x, y) = OrderedPair(promote(x, y)...)
This way you can freely skip checking conditions when you know they’ve been met elsewhere in your code without moving everything to an inner constructor. You can name the inner constructor whatever you want but the naming convention ensures downstream users don’t have any excuses for inappropriately bypassing checks.
Significantly more evily you can directly generate calls to Expr(:new, T, args...) and to Expr{:splatnew, T, args) e.g. with generated functions or other metaprogramming.
julia> struct OrderedPair
x::Real
y::Real
OrderedPair(x,y) = x > y ? error("out of order") : new(x,y)
end
julia> @generated direct_new(B::Type, args...) = Expr(:splatnew, :B, :args)
direct_new (generic function with 1 method)
julia> Base.copy(op::OrderedPair) = direct_new(OrderedPair, op.x, op.y)
julia> op1 = OrderedPair(10, 20)
OrderedPair(10, 20)
julia> copy(op1)
OrderedPair(10, 20)
julia> @which copy(op1)
copy(op::OrderedPair)
@ Main REPL[3]:1
Not a problem, per se, but just that in the situation as posed in the first post there’s no need for bypassing the inner constructor as you could just define copy(op::OrderedPair) = op and move on.
It was only in the next post that it was clarified that the actual use case was more complex.
Minor stylistic point, but you’re using the Val here as a trait (sort of, you’re using it for compile-time dispatch on a type), so it should probably come first in order:
struct OrderedPair
x::Real
y::Real
OrderedPair(::Val{True}, x, y) = new(op.x, op.y)
OrderedPair(::Val{False}, x, y) = x > y ? error("out of order") : new(x,y)
OrderdPair(x, y) = OrderedPair(Val(false), x, y) # could probably also have this as an outer constructor
end
This also shows the convention that arguments which aren’t directly referenced because they’re used for their type don’t receive names.
Nothing bad will happen the way you wrote it, of course, this is just a matter of idiom.