Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

julialang: can (should) this type error be caught at compile time?

function somefun()
    x::Int = 1
    x = 0.5
end

this compiles with no warning. of course calling it produces an InexactError: Int64(0.5). question: can you enforce a compile time check?

like image 801
mrchance Avatar asked Mar 04 '23 20:03

mrchance


2 Answers

Julia is a dynamic language in this sense. So, no, it appears you cannot detect if the result of an assignment will result in such an error without running the function first, as this kind of type checking is done at runtime.

I wasn't sure myself, so I wrapped this function in a module to force (pre)compilation in the absence of running the function, and the result was that no error was thrown, which confirms this idea. (see here if you want to see what I mean by this).

Having said this, to answer the spirit of your question: is there a way to avoid such obscure runtime errors from creeping up in unexpected ways?

Yes there is. Consider the following two, almost equivalent functions:

function fun1(x     ); y::Int = x; return y; end;
function fun2(x::Int); y::Int = x; return y; end;

fun1(0.5)   # ERROR: InexactError: Int64(0.5)
fun2(0.5)   # ERROR: MethodError: no method matching fun2(::Float64)

You may think, big deal, we exchanged one error for another. But this is not the case. In the first instance, you don't know that your input will cause a problem until the point where it gets used in the function. Whereas in the second case, you are effectively enforcing a type check at the point of calling the function.

This is a trivial example of programming "by contract", by making use of Julia's elegant type-checking system. See Design By Contract for details.

So the answer to your question is, yes, if you rethink your design and follow good programming practices, such that this kind of errors are caught early on, then you can avoid having them occuring later on in obscure scenarios where they are hard to fix or detect.

The Julia manual provides a style guide which may also be of help (the example I give above is right at the top!).

like image 165
Tasos Papastylianou Avatar answered May 11 '23 00:05

Tasos Papastylianou


It's worth thinking through what "compile time" really is in Julia — because it's probably not what you're thinking.

When you define the function:

julia> function somefun()
           x::Int = 1
           x = 0.5
       end
somefun (generic function with 1 method)

You are not compiling it. Julia won't compile it, in fact, until you call it. Julia's compiler can be thought of as Just-Barely-Ahead-Of-Time, standing in contrast to typical JIT or AOT designs.

Now, when you call the function it compiles it and then runs it which throws the error. You can see this compilation happening the very first time you call the function — it takes a bit more time and memory as it generates and caches the specialized code:

julia> @time try somefun() catch end
  0.005828 seconds (6.76 k allocations: 400.791 KiB)

julia> @time try somefun() catch end
  0.000107 seconds (6 allocations: 208 bytes)

So perhaps you can see that with Julia's compilation model it doesn't so much matter if it gets caught at compile time or not — even if Julia refused to compile (and cache) the code it'd behave exactly like what you currently see. It'd still allow you to define the function in the first place, and it'd still only throw its error upon calling the function.

The question you mean to ask is if Julia could (or should) catch this error at function definition time. And then the question is really — is it ok to define a method that always results in an error? What about a function like error itself? In Julia, it's totally fine to define a method that unconditionally errors like this one, and there can be good reasons to do so.

Now, there are ways to ask Julia if it is able to detect that this method will always unconditionally error:

julia> @code_typed somefun()
CodeInfo(
1 ─     invoke Base.convert(Main.Int::Type{Int64}, 0.5::Float64)::Union{}
└──     $(Expr(:unreachable))::Union{}
) => Union{}

This is the very first step in Julia's process of compilation, and in this case it can see that everything beyond convert(Int, 0.5) is unreachable — that is, it errors. Further, it knows that since the function will never return, it's return type is Union{} (that is, no possible type can ever be returned!) So you can ask Julia to do this step with, for example, the @inferred macro as part of a test suite.

like image 21
mbauman Avatar answered May 11 '23 00:05

mbauman