As an aside, @code_warntype
is not always entirely truthful. But here, it is true that the name y
can have either of those types across the entire life of the function. However, here there is no ambiguity as to what it is at any particular time. And the @code_warntype
does clearly show that it knows the return value is a Vector{String}
.
A variable is not a place in memory, so a variable that takes multiple types does not need to be able to store and interchange those types arbitrarily. It is simply a name (“binding”) that you can use to refer to some datum. Just because I can put a surfboard in my car for one trip and a snowboard in my car for another doesn’t mean I can’t know which it holds at any given time.
julia> @code_typed g()
CodeInfo(
1 ─ %1 = Core.tuple("a", "b")::Tuple{String, String}
│ %2 = $(Expr(:foreigncall, :(:jl_alloc_array_1d), Vector{String}, svec(Any, Int64), 0, :(:ccall), Vector{String}, 2, 2))::Vector{String}
└── goto #7 if not true
2 ┄ %4 = φ (#1 => 1, #6 => %13)::Int64
│ %5 = φ (#1 => 1, #6 => %14)::Int64
│ %6 = Base.getfield(%1, %4, false)::String
│ Base.arrayset(false, %2, %6, %4)::Vector{String}
│ %8 = (%5 === 2)::Bool
└── goto #4 if not %8
3 ─ goto #5
4 ─ %11 = Base.add_int(%5, 1)::Int64
└── goto #5
5 ┄ %13 = φ (#4 => %11)::Int64
│ %14 = φ (#4 => %11)::Int64
│ %15 = φ (#3 => true, #4 => false)::Bool
│ %16 = Base.not_int(%15)::Bool
└── goto #7 if not %16
6 ─ goto #2
7 ┄ goto #8
8 ─ return %2
) => Vector{String}
Not only is there no ambiguity, Float64
never even appears here because it’s part of dead code that gets eliminated.
It’s hard to imagine that no one at your company will ever want to call the same code with different types. What about a function that finds the smallest positive value in a collection? Do you really want people to write multiple versions of that function that are explicitly annoted to operate on each of Vector{Float64}
, Vector{Int64}
, SVector{3,Float64}
, and SubArray{Float64, 1, Matrix{Float64}, Tuple{UnitRange{Int64}, Int64}, true}
? And what if they recognize a bug and fix it in most of those, but accidentally miss one where it persists for goodness-knows how long? That’s where maintainability really starts to crumble. There’s even a principle for this in software engineering. This proposal is not entirely maintainability up-side.
A few strategically-placed comments at confusing places are much more informative than exhaustive (and exhausting) annotation. Types are usually the least interesting part of code anyway – what/why/how the code does things is the part that’s actually important to communicate.
As commenters indicated above, a RHS type annotation is actually a stronger assertion than one on the LHS, because a LHS annotation will attempt to convert
the value and only throw if that fails. The RHS annotation will fail without recourse if it is mislabeled. So requiring all RHS to be annotated would result in stricter typing requirements than LHS. Besides, it sounds like your proposal is to require annotations on every LHS so it’s not like it’s fewer annotations than RHS would be.
julia> x1::Int = 3.0
3.0
julia> x2 = 3.0::Int
ERROR: TypeError: in typeassert, expected Int64, got a value of type Float64
If you are too onerous to your users and fail to provide aggressive supervision, they can simply satisfy the letter (but not spirit) of your requirements by annotating every variable ::Any
function h()
y::Any = 4.0
return y
end
And yet, despite the ::Any
(or a ::Real
) annotation, the @code_warntype
is not fooled and knows y
is specifically a Float64
. These ::Any
annotations are harmless and effectless except for adding work for the parser and compiler.