Let’s work with an example. Say I have two different units of length.
abstract type Length end
struct Meters <: Length
value::Float64
end
struct CentiMeters <: Length
value::Float64
end
I want to use the existing Float64 operations to do my calculations, so for convenience, I define the following conversion rules. Note that we do all calculations at the centimeter scale:
Base.convert(::Type{Float64}, x::Meters) = x.value * 100 # when in Float64-land, always represent length in centimeters
Base.convert(::Type{Float64}, x::CentiMeters) = x.value
Base.convert(::Type{Meters}, x::CentiMeters) = Meters(x.value / 100)
Base.convert(::Type{CentiMeters}, x::Meters) = CentiMeters(x.value * 100)
I want to be able to add two lengths of any unit. So:
Base.:+(a::Length, b::Length) = CentiMeters(convert(Float64, a) + convert(Float64, b))
Calculations are now convenient.
x = CentiMeters(100) + Meters(2) # defaults to CentiMeters
y::Meters = CentiMeters(100) + Meters(2) # but I can get Meters if I want (via implicit conversion and type assertion).
@assert x isa CentiMeters && x.value == 300
@assert y isa Meters && y.value == 3
On person’s convenience can be another person’s confusion:
lengths = Float64[]
push!(lengths, Meters(1))
push!(lengths, CentiMeters(1))
@assert lengths == [100, 1]
But implicit conversion can be problematic.
Implicit conversion, when combined with the dual meaning of ::, can be confusing. see discussion here: Semantics of :: in return type vs. argument type annotations
must_be_meters_1(x)::Meters = x # this converts and asserts
must_be_meters_2(x) = x::Meters # this asserts
@assert must_be_meters_1(CentiMeters(1)) isa Meters
# @assert must_be_meters_2(CentiMeters(1)) isa Meters # fails
Implicit conversion can result in logic bugs, such as this one:
length = CentiMeters(100) |> Meters # I never defined a `Meters(::CentiMeters)` constructor.
@assert length == Meters(100) # 100 centemeters is not the same as 100 meters
In other cases, it can lead to data being copied unintentionally, such as when an array view is implicitly converted to (and hence copied) as an array. See here: Why does this conversion happen implicitly?
A layer of indirection could allow us to “switch off” implicit conversion. We could define:
Base.implicit_convert(args...) = convert(args...)
All places that use conversion implicitly can then call implicit_convert instead of convert.
And for our own types, we could:
Base.implicit_convert(Type{Meters}, x) = x
Base.implicit_convert(t, x::Meters) = x
This would allow each of the following lines of code to error:
x::Meters = CentiMeters(10)
f(x:Meters)::CentiMeters = x
length = CentiMeters(100) |> Meters
This would be:
- non breaking
- allow only certain, important types to be “switched off” while maintaining the convenience of implicit conversion by default
- allow a much stricter application where implicit conversion is turned off for all types (
strictmode feature?)
I think folks who come from statically typed languages would appreciate this option. I certainly would.