How is it that new Julia programmers tend to abuse type annotations?

I’m sure it helps some people, but it certainly doesn’t solve the problem. The fundamental problem is just lack of knowledge. The solution is learning more, and that takes time. Tooling that displays information in a more usable way is of course useful for learning, but it won’t end the problem (especially because most newcomers have never heard of Cthulhu)

2 Likes

I agree. Also, superfluous type annotations are one of the first habits that users coming from other programming languages drop. No harm done, other than a bit of extra wear and tear on the keyboard. [I wrote “extra typing” first then reread the sentence. :wink:]

8 Likes

One of the first things I learned in TypeScript was to add enough type annotations to prevent the compiler from inferring any of my variables as Any. To do this, I needed a tool that would give me real-time feedback about whether a variable was inferred as Any or not. With that information, I quickly developed the intuition to know where I should add type annotations.

I can imagine that newcomers to Julia might experience the same learning process if the Julia VS Code extension provides real-time feedback about whether a variable is inferred as Any or not.

9 Likes

I think the Julia equivalent of that is using @code_warntype and fixing your code until the red or yellow text goes away. I wish it would do more to highlight abstract parameters such as in Vector{Any}.

image

1 Like

Well that’s not showing anything red because you didn’t do anything involving vec that would cause any performance trouble.

1 Like

One would prefer likely prefer foo to return Vector{Float64}. Coming from another language, it may be confusing that Julia does not infer a more specific element type such in bar below.

julia> bar(x) = [x, 5, 3.5]
bar (generic function with 1 method)

julia> bar(2)
3-element Vector{Float64}:
 2.0
 5.0
 3.5

julia> foo(2)
3-element Vector{Any}:
 2
 5
 3.5

I understand what you’re getting at, but in this case I don’t think it has anything to do with type inference. It just depends on whether the user understands that [] is syntax sugar for Any[].

4 Likes

It depends on the language semnatics. Consider this type inference example from Rust:

fn main() {
    // Because of the annotation, the compiler knows that `elem` has type u8.
    let elem = 5u8;

    // Create an empty vector (a growable array).
    let mut vec = Vec::new();
    // At this point the compiler doesn't know the exact type of `vec`, it
    // just knows that it's a vector of something (`Vec<_>`).

    // Insert `elem` in the vector.
    vec.push(elem);
    // Aha! Now the compiler knows that `vec` is a vector of `u8`s (`Vec<u8>`)
    // TODO ^ Try commenting out the `vec.push(elem)` line

    println!("{:?}", vec);
}

A Rust user coming to Julia might wonder why Julian type inference is not as smart.

2 Likes

But that would be a misunderstanding of Julia semantics. In Julia, the element type of an array is determined at construction time—pushing into a Julia vector will never change the type of a vector. There is no type inference involved in determining the element type of a vector. Since [] is sugar for Vector{Any}(), the element type must be Any.

8 Likes

Yeah, the type inference here is a distraction. If we had chosen Vector semantics that behaved like Rust’s vector, the compiler would have no trouble analyzing it in the exact same way.

julia> using MicroCollections, BangBang

julia> code_typed(Tuple{}; optimize=false) do 
           elem = Int8(5)
           vec = EmptyVector()
           vec = push!!(vec, elem)
       end
1-element Vector{Any}:
 CodeInfo(
1 ─      (elem = Main.Int8(5))::Core.Const(5)
│        (vec = Main.EmptyVector())::Core.Const(Union{}[])
│   %3 = Main.push!!(vec::Core.Const(Union{}[]), elem::Core.Const(5))::Vector{Int8}
│        (vec = %3)::Vector{Int8}
└──      return %3
) => Vector{Int8}
4 Likes

I’d like to add another possibility (couldn’t find it after skimming through the answers): in the “static typing” camp, type annotation is considered helpful, for this help the programmer to understand what arguments the function is accepting. Thus, if one’s not well versed into the abstract type notion, it’s possible one’s unwillingly over-restricting the method as a result of using concrete types.

Put it differently, getting a clean code is considered good practice (if not accepted as a default, then painfully learnt during your early carrer), and some think static typing helps for that (see e.g. rationale behind TypeScript), hence naive but clumsy type annotation. Then, you eventually learn to think through type hierarchies, and stop over-specifying types.

4 Likes

On the other hand, my recommendation is that controlling the “Type” I am using to represent my data is NOT an option, it is always necessary. Even if you don’t explicitly state it. Precisely because we want to have maximum confidence in the calculations we perform.
The “Julia default type” in the function parameters is just an option.

1 Like

Naturally you can use Julia the way you like, but note that because of multiple dispatch, it is idiomatic to write Julia code for which you simply will not know the type of function arguments.

But that is fine, all you need to assume is that they satisfy some (informally defined) interface. This allows a method from package A to work on arguments defined in packages B and C, withoutcooperation from said packages for this explicit purpose.

2 Likes

I allways know the nature of the input data. I allways study which is the best computer representation for the operations to avoid errors on intermediate operations. And I allways have a sample of similar final data.
Then you can try the code and results with a Concrete Type.
If you don’t know enough about the input data… you must research before.

Good for you! Again, it is possible to program in Julia with that approach, but you are not reaping the benefits of the language. You might as well just use C.

Eg consider

nonsense(x, y, z) = √(x + y^2) / z

which, as defined, will work with all kinds of real number type (floats, incl BigFloat if you want to test), ForwardDiff.Dual (which usually adds a type tag from the outermost caller), complex numbers, quaternions, etc. So you can write it once, and reuse.

That’s a little too harsh, I think. Sometimes you are writing code that you want to re-use over and over, in which case you want to be generic, and sometimes you are writing code that’s designed only for a specific project. Often, you decide after the fact that you want to re-use the latter, in which case Julia allows you to gradually make it more generic. And even for single-use code, you still benefit from re-usable code that other projects developed, as well as interactivity and high-level features of Julia that are not available in C.

If you feel more comfortable programming Julia with explicit concrete type declarations for everything, that’s totally fine. Just be aware that this is not generally necessary for performance (at least for local variables and function arguments), and that you may want to relax your types at some point to allow the code to become more generic.

26 Likes

Indeed. Sorry being sarcastic. I was triggered by the “I always know everything”, but should have responded more gently.

8 Likes

4 posts were split to a new topic: Learning Julia for scientists who are beginning programmers

Mmmmmm, I don’t think so “strait”,
For example in some cases I work with “Natural” numeric data, so for the definition of the functions I use arguments typed with “Unsigned” Abstract types, with this I design functions that are valid for all the “Concrete SubTypes” (“UInt…” ) in which I am interested. But I take care that all intermediate operations are performed on the Concrete Subtype of the data passed as argument.
I check that there are no “Overflows” because Julia does not warn.
The habit of declaring the function argument with an Abstract Type such that it sufficiently captures the different concrete subtypes that I am going to use into the function, gives me trust and good results.

 function consulta(m::Unsigned, Db7::IdDict)
        mtype=typeof(m)
        Db7keytype=Base.keytype(Db7)
        ..............

I put type annotations on function parameters, like AbstractString, AbstractArray, etc just as a hint (for myself) on what type does this function expect. Trying not to restrict it too much at same time. Probably, a proper doc string would be a better approach.

2 Likes