Yes, exactly! So you want two functions: One named implicit_convert that gets used in default constructors and assertions and a number of generic definitions, and one named explicit_convert that is never called by base.
That’s exactly the status quo! It’s just that the former is called convert, and the latter is left to you to define as you wish.
That would not solve the problem. I would need base to call some Base.convert1 when I push a something into an array and Base.convert2 when I use default constructors. That way I could keep convert1 behavior by defining convert1(t, x::MyType) = my_convert(t, x) while switching off convert2 by defining convert2(t, x::MyType) = identity(x). Then I get the error I want when the wrong argument is passed to the constructor when a MyType is expected, while letting me pass the “wrong” type to a vector and have it be accepted as MyType.
Well, the constructor case is easy to override with a custom constructor, but I get your general point.
I think the trouble is stemming from wanting to have what Base Julia would consider as a “surprising” convert in Julia’s builtin containers’ setindex! — and only in that one case. And, unfortunately, unlike arithmetic, promotion, and constructors, that’s a case where it’s not really possible to customize and something different than convert.
This is not true. All prior code that assumed implicit conversion is exactly the same as manual convert calls, as documented, glitch when composed with a new type that opts out of this fallback and strictly implements convert for explicit conversion. They are forced to replace all the calls with implicit_convert to reflect their intent and properly error with the new types, as opposed to unlucky users hitting an implicit conversion error 2 days into a simulation. v1.3+ libraries failing with v1.15+ libraries is not backwards compatibility. Again, an explicit_convert would not cause this problem.
This is also not true. getproperty for dot syntax and getfield were designed for v0.7 when the API was unstable, and this has not changed in v1’s stable API.
Implicit conversion is supposed to be all-or-nothing and independent of type. If you toggled implicit conversion at the 6+ contexts for each type, making 2^6=64 configurations, type-generic code using implicit conversion would be incredibly brittle, and the only “fix” would be to dispatch on sets of 64 Holy traits for very slightly different behaviors. I don’t think people who sometimes avoid dealing with strong typing would rather deal with type-dependent implicit conversion. If you want conversions to only occur in some contexts but not others, implement and use explicit type checks and conversions (constructors, a explicit_convert-like function) instead of Base.convert.
If you feel motivated to play around with this idea, one thing you can do is create compilation contexts where the rules you want are enforced.
E.g. probably the easiest way to do this is with IRTools.jl:
using IRTools: IRTools, IR, @dynamo, recurse!
@dynamo function no_implict(args...)
ir = IR(args...)
ir == nothing && return
recurse!(ir)
return ir
end
function no_implict(::typeof(Base.setindex!), collection::AbstractArray{T}, elem::U, inds...) where {T, U}
if !(T <: U)
error("That's not allowed in this mode!")
end
setindex!(collection, elem, inds...)
end
function no_implict(::typeof(Base.push!), collection::AbstractArray{T}, elem::U) where {T, U}
if !(T <: U)
error("That's not allowed in this mode!")
end
push!(collection, elem)
end
and then tada!
julia> no_implict() do
v = [1,2,3]
push!(v, 4.0)
end
ERROR: That's not allowed in this mode!
Stacktrace:
[1] error(s::String)
@ Base ./error.jl:44
[2] no_implict(::typeof(push!), collection::Vector{Int64}, elem::Float64)
@ Main ~/Nextcloud/Julia/scrap/implicit_convert/scrap.jl:19
[3] #20
@ ./REPL[10]:3 [inlined]
[4] no_implict(args::var"#20#21")
@ Main ~/.julia/packages/IRTools/v0mn8/src/reflection/dynamo.jl:0
[5] top-level scope
@ REPL[10]:1