Search code examples
compilationjulialintstatic-typingdynamic-typing

Requiring type declaration in Julia


Is there any way to explicitly require in Julia (e.g. say within a module or package) that types must be declared? Does e.g. PackageCompiler or Lint.jl have any support for such checks? More broadly, does the Julia standard distribution itself provide any static code analyzer or equivalent that could help check this requirement?

As a motivating example, say we want to make sure that our growing production code base only accepts code that is always type declared, under the hypothesis that large code bases with type declarations tend to be more maintainable.

If we want to enforce that condition, does Julia in its standard distribution provide any mechanisms to require type declaration or help advance that goal? (e.g. anything that could be checked via linters, commit hooks, or equivalent?)


Solution

  • The short answer is: no, there is currently no tooling for type checking your Julia code. It is possible in principle, however, and some work has been done in this direction in the past, but there isn't a good way to do it right now.

    The longer answer is that "type annotations" are a red herring here, what you really want is type checking, so the broader part of your question is actually the right question. I can talk a little bit about why type annotations are a red herring, some other things that aren't the right solution, and what the right kind of solution would look like.

    Requiring type annotations probably doesn't accomplish what you want: one could just put ::Any on any field, argument or expression and it would have a type annotation, but not one that tells you or the compiler anything useful about the actual type of that thing. It adds a lot of visual noise without actually adding any information.

    What about requiring concrete type annotations? That rules out just putting ::Any on everything (which is what Julia implicitly does anyway). However, there are many perfectly valid uses of abstract types that this would make illegal. For example, the definition of the identity function is

    identity(x) = x
    

    What concrete type annotation would you put on x under this requirement? The definition applies for any x, regardless of type—that's kind of the point of the function. The only type annotation that is correct is x::Any. This is not an anomaly: there are many function definitions that require abstract types in order to be correct, so forcing those to use concrete types would be quite limiting in terms of what kind of Julia code one can write.

    There's a notion of "type stability" that is often talked about in Julia. The term appears to have originated in the Julia community, but has been picked up by other dynamic language communities, like R. It's a little tricky to define, but it roughly means that if you know the concrete types of the arguments of a method, you know the type of its return value as well. Even if a method is type stable, that's not quite enough to guarantee that it would type check because type stability doesn't talk about any rules for deciding whether something type checks or not. But this is getting in the right direction: you'd like to be able to check that each method definition is type stable.

    You many not want to require type stability, even if you could. Since Julia 1.0, it has become common to use small unions. This started with the redesign of the iteration protocol, which now uses nothing to indicate that iteration is done versus returning a (value, state) tuple when there are more values to iterate. The find* functions in the standard library also use a return value of nothing to indicate that no value has been found. These are technically type instabilities, but they are intentional and the compiler is quite good at reasoning about them optimizing around the instability. So at least small unions probably must be allowed in code. Moreover, there's no clear place to draw the line. Although perhaps one could say that a return type of Union{Nothing, T} is acceptable, but not anything more unpredictable than that.

    What you probably really want, however, rather than requiring type annotations or type stability, is to have a tool that will check that your code cannot throw method errors, or perhaps more broadly that it will not throw any kind of unexpected error. The compiler can often precisely determine which method will be called at each call site, or at least narrow it down to a couple of methods. That's how it generates fast code—full dynamic dispatch is very slow (much slower than vtables in C++, for example). If you have written incorrect code, on the other hand, the compiler may emit an unconditional error: the compiler knows you made a mistake but doesn't tell you until runtime since those are the language semantics. One could require that the compiler be able to determine which methods might be called at each call site: that would guarantee that the code will be fast and that there are no method errors. That's what a good type checking tool for Julia should do. There's a great foundation for this sort of thing since the compiler already does much of this work as part of the process of generating code.