On type annotations

But we want them to be supported by other packages inside the company. And also, we want to use functionality provided by other open source packages.

Thank you all for sharing your expertise. I think everything has been said about this topic now. On my side an evaluation of the input will be made and we will rethink the coding standard.

4 Likes

No, Unions are not the solution for everything. Abstract interfaces are for the most part and sometimes unions.

2 Likes

This style guide, at its best, seems to really be an attempt to retrofit static typing on top of Julia. In any case perhaps it’d be good to take a step back and analyze the requirements here.

What is the actual goal here? Is it the compile-time safety allowed by static typing? Or is it just that the relevant programmers are familiar with statically-typed languages and can’t be bothered with learning Julia properly?

Honestly, I think it’s the latter, in which case it’d be better to either invest in learning Julia, writing good test-suites, etc.; or switch to C++ or something.

If it’s the former:

  1. To get the safety benefits of static typing (error before run time), what is required is a sound static analyzer. A sound type checker of a Turing-complete language necessarily rejects a lot of valid programs, effectively relinquishing a lot of expressive/descriptive power: Chapter 1 — Program Analysis
  2. A lot of programming languages that are commonly considered as “statically typed” don’t actually have sound type checkers, so they don’t actually provide the advertised safety (they may error at run time when a programmer would expect a compile-time error).
  3. A useful sound static type checker for Julia would perhaps be a research project. The next best thing is JET.jl, by @aviatesk.

I’m not able to give a good suggestion here, but I think a workflow where each method has to be either:

  1. cleared by JET.jl for the declared argument types, or
  2. somehow marked as “unsafe” (in the Rust sense)

… would be better than requiring this coding standard.

4 Likes

Your example shows literal construction, which is unrelated to the input arguments. The problem arises when assignment assertions depend on input arguments. And with union inputs those can’t be hard-coded.

So

function foo(x::Float64) 
    y::Float64 = x + 1
   ... 
end

Is possible, but this fails with unions. Do you only need assignment assertions with literals? Why not with the above?

@nsajko This is a very insightful comment. Thanks!
The only problem with JET.jl is that it doesn’t report exactly what companies like TIOBE expect for their checkers. Also, it is not a paragon of user-friendliness

2 Likes

My current sense is that while the above is the intention, the proposed solution of a left hand type assertion, binding the variable to the type, also has the effect of inviting conversion when it is not desired.

In a prior discussion, a @returnassert macro was developed to just assert the return type of a function, without any implicit conversion.

The above line invites implicit conversion. A macro that rewrites all left-hand assertions to be as follows or lowers the left-hand assertions and then removes the conversions might be a more appropriate tool for static typing.

    other = calculate_other_struct(value, obj, vec)
    other::OtherStruct

Here we ensure both that

  1. other does not change type
  2. The right hand side also does not change type.

What I imagine with the large number of structs is there actually may be a need to convert between the structs and thus unintentional conversion could be a real issue for these structs, not just numeric types.

In summary, my thought here is that we need another tool here other than the left-hand variable assertion or the function return type assertion. In the near term, this may mean a specialized macro, like the referenced @returnassert to accomplish this goal without the implicit conversion side effects. In the long term, we may need a different kind of type assertion or assignment syntax.

Somewhat missing from the discussion are the use of type parameters to both enforce expectations and provide flexibility.

5 Likes

For what it’s worth you can do things like

function foo(x::Union{Float32, Float64})
    T = typeof(x)
    y::T = x + 1
end

although it would have to be T::DataType = typeof(x) to follow the proposed standard.

3 Likes

Well, to you’re right in this case, I’ve just replicated the Method error, but one can talk about the interface, reference the docs, or provide a suggestion about the method that needs to be defined. When I first started developing in Julia, I hit MethodErrors all of the time and they felt inscrutable.

Writing these reminders to myself were invaluable.

2 Likes

Makes no difference to the topic of type annotations and dispatch, and the base numerical types only served as accessible examples. Obviously nobody outside your company knows the structs you’re talking about.

Can sorta pull this off in one go for a main function but doesn’t cover anything else done interactively.

I do not intend to derail the discussion, and I feel that this intervention is relevant to the OP as well. This being said…

I understand the usability compromise, but I am pretty unease that simply being explicit about types can end up in:

  • negatively impacted performance
  • unexpected errors

As long as there is no violation of the business logic of the said code, it seems like - at least for sanity’s sake - type annotation should do no harm (I am not challenging your claims - I understand that annotating stuff exhaustively actually harms).

In a way, it feels like “prohibited type annotation” and less “optional type annotation” :slight_smile:

5 Likes

I’m glad you said it - I have this feeling too, though lack the confidence in my understanding to say so.

I think two things are true in this thread

  1. There are many good reasons not to do tons of type annotation - I think the general advice we give users on this point is right, particularly for open source libraries that expect to work with a lot of the ecosystem. The thread is filled with great examples of this point.
  2. If the company sees benefit to this style for their internal code, and are aware of the limitations, by all means do what works for you, and thanks for using Julia in production! :smile:
3 Likes

As extensively discussed, putting a type annotation on a method return type or variable is not “simply being explicit about types”.

I’ll agree though, that type annotations calling convert wasn’t a good design. Hopefully something to consider for Julia v2.

7 Likes

I’m on the fence about this for fields and array elements because implicit conversion there in several languages is about as routine as promotion in base operations, but it really does seem like people expect and want an error, ideally before happening upon it at runtime, at annotated variables and return types, even in the cases where convert is implemented. I know the argument that conversion by annotated return type is more convenient than doing it at every exit point, but incorporating the conversion into the method is a very rarely used feature that I actually wouldn’t mind writing out explicitly.

1 Like

I think even new calls convert. It’s way too difficult to avoid convert.

Here is a small experiment (by a physicist):

function distance_to_target_nota(t) # no type annotations
    c = 299792458e0 # speed of light
    # ... lot of code ...
    # developer does not realize that c is already used
    # remaining comments are "real" for this experimental nonsense function
    #
    foo1(t, coeff) = (t-1)*coeff      # effect of warp thruster
    foo2(t, coeff) = (t-1)*coeff*1.1  # warp thruster 2 is slightly better
    dx = 0
    if t>1   # if time too large then warp is used:
        c = 3000 # efficiency coefficient as specified
        dx = foo1(t, c) + foo2(t, c)
    end
    # ... more code ...
    
    return c*t-dx
end

has a programming error (reuses variable c) and returns a by five orders of magnitude wrong result for t>1. The type annotated version

function distance_to_target_withta(t::Float64)::Float64 # with type annotations
    c::Float64 = 299792458 # speed of light
    # ... lot of code ...
    # developer does not realize that c is already used
    # remaining comments are "real" for this experimencal nonsense function
    #
    foo1(t::Float64, coeff::Float64)::Float64 = (t-1)*coeff      # effect of warp thruster
    foo2(t::Float64, coeff::Float64)::Float64 = (t-1)*coeff*1.1  # warp thruster 2 is slightly better
    dx::Float64 = 0
    if t>1   # if time is too large then warp is used:
        c::Float64 = 3000 # efficiency coefficient as specified
        dx = foo1(t, c) + foo2(t, c)
    end
    # ... more code ...
    
    return c*t-dx
end

does not compile and so catches this particular error.

The OP cites “readability and maintainability” as justification for using type annotations everywhere in Julia. This is perhaps debatable, but, I think, they are a tool to prevent some of the common programming errors (similar as in other statically typed languages). I agree that the type annotations are not ideal for this (being too much cluttering etc.). A discussion of other available tools, @returnassert , @inferred from Test.jl, …, and other code analysis tools would be useful, perhaps in another or forked thread.

1 Like

Not a method compilation error, the syntax error is thrown upon evaluating the method. This is just repurposing a type declaration limitation to error at accidental reassignment in a long stretch of code instead of using conventional programming practices like making a new local variables in a new local scope (let), splitting a large method into calls of smaller methods, and descriptively disambiguating names. It doesn’t work for something as simple as accidentally reassigned arguments:

julia> function energy(mass, c::Float64=299792458.0)
         if mass < 0 # sleep deprived code incoming
           c::Int=-1 # inferred ::Union{Float64, Int64}
           mass = c*mass
         end
         mass*c^2
       end
energy (generic function with 2 methods)

julia> energy(2)
1.7975103574736352e17

julia> energy(-2)
2
2 Likes
#!/usr/bin/env julia

function energy(c::Float64)::Float64
    c::String="bbb"
    println(typeof(c))
    return 5.0
end

energy(5.0)

Indeed, this just runs without any problems. I did not expect this. I wonder if this is documented and desired Julia behavior or a bug in the language that can be fixed now it is discovered.

1 Like

Why not? Looks perfectly fine in my eyes.

The argument type annotation is not a local variable type assertion.

You cannot do a new local declaration.

julia> function energy(c::Float64)
           local c::String
       end
ERROR: syntax: local variable name "c" conflicts with an argument
4 Likes