You cannot really. The goal of ::Int64 is to ensure that that variable never changes its type away from an Int64 and code might have been compiled with that assumption.
And why does this does not raise an error? My intention often with type signatures in function arguments is also to try for type stability but than âŚ
julia> foo(x::Int=0) = x=1.5
foo (generic function with 2 methods)
julia> foo(1.5)
ERROR: MethodError: no method matching foo(::Float64)
...
julia> foo(1)
1.5
What you guarantee with the type signature is that the input value has to be of type Int. Internally, you are just assigning the label x to a new value. If you want that x to be Int, you need to annotate the type of that assignment as well:
julia> function foo(x::Int)
x::Int = 1.5
end
foo (generic function with 1 method)
julia> foo(1)
ERROR: InexactError: Int64(1.5)
(although flooding the code with these annotations is not really the idiomatic way to write type stable code in Julia, it would be very inconvenient and be against the nature of language).
Think of it as ânames in boxesâ.
The function is a box. When you look for a name, it might look âoutside the boxâ e.g. at its arguments, there it would find x::Int
If you define x= 1.5 then in the âfunction boxâ you have a new name x (that the function would look first for) that is now a float. You basically introduced a local new name for something and gave it a type â that are different from the ones in the signature.
You can for example avoid that way to âaccidentally overwrite/mutateâ your input. There this might be very useful.
This âlocal name of the second xâ is also forgotten afterwards again (at the end of the function).
Whether it might also be confusing with different types is of course a different story, but there might be good reasons to introduce locally something new under the name x.
Julia allows annotating types in function parameters because it is necessary for multiple dispatch (one of the most, if not the most, important trait of the language). Annotating types of global variables to disallow them changing type was introduced a long time after. And, if I am not wrong, annotating the type of local scope variables was introduced even later. So, I think the confusion is related to the fact that the two mechanisms have nothing to do with each other, and one did not exist until recently. Julia could now change the semantics of the language and make every function automatically annotate the type of each parameter inside the local scope preventing type change, but I think this would be breaking and would require Julia 2.0.
Honestly, I find it incomprehensible that we can impose that a variable has to dress with a certain cloth to enter that box (the function) but once inside it can change clothes to what ever it wants.
I couldnât believe this one, but my computer confirmed it!!! So we impose a type and next that is ignored?
Edit: I see a = 2.0 is converted to Int. a = 2.1 errors.
I think that would be useful, but still very limited. One could still migrate to a type-unstable assignment with:
function f()
x::Int = 1
y = x
y = 2.1
return y
end
Thus, the scope of the stability provided by the input value would be limited. The first time I heard about Julia (I guess back in 2015) it was suggested that something like this could be written:
julia> function f(x_in:Int)
local x::Int = x_in
local y::Int
y = x
y = 2.1
return y
end
f (generic function with 1 method)
julia> f()
ERROR: InexactError: Int64(2.1)
Giving to the user the âfeelâ of typed language if one wants to (it would be complete if the function compilation threw an error if any label is not declared with an explicit type). But even if that can have benefits in terms of some debugging (strictly typed languages do have these advantages), Iâm not sure if I would have migrated from Fortran if that was required for a good programming workflow. Still, having a a sub-language with these guarantees would be certainly a welcome addition (I wouldnât mind having @strictly_typed macro for that, and probably would find some use to it).
Yes, a macro seems the best tool for an opt-in feature to have multiple dispatch annotations also restrict the variable types inside the functionâs local scopes.
That sounds like the most natural thing in the world to me.
Though, in fact it is a value that you are letting in through the door, not a variable. That you later bind that name to a different value is also pretty normal for a dynamically typed language.
If thereâs anything Iâm uncomfortable with, itâs the opposite. It does not seem right for a dynamic language to put the type on the variable, which is now permissible in some circumstances.
I have close to zero experience writing macros, but that seems feasible, doesnât it? A macro that checks, first, that the input values of a function are all concrete. Then, checks if every label inside the function is always associated to the same type, and if the value is mutable, to the same value. That doesnât seem too different from what the inference machinery does, or what @code_warntype, Chutlhu and JET can do.
Probably the fact that this macro doesnât exist is a sign that when one gets used to Julia we donât want to write code with strict types anymore, ever (and that those tools already do a descent job in solving type-inference issues).
I know. And in principle, yes Though that would be pretty inconvenient.
I just said I was uncomfortable with it because it messes with my (surely incomplete) mental model, which defines dynamically typed languages as âtypes belong to values, not variablesâ.
Those arenât things that macros can do by themselves â macros donât know about types or inference, they only know syntax (how things are âspelledâ). @code_warntype is a trivial macro that just rewrites an expression into a call to a function that hooks into the compiler, itâs not doing any of the type checking itself:
I look at it like this: The function signature foo(x::Int) is a filter on what type of values (not variables) are accepted through the door. Once inside, the value is offered a jacket (âxâ). This jacket does not belong to the value, and the jacket can be put on a different âguestâ (or on the back of a chair) at a later point.
Remember, when you pass a value to a function, referred to by a name, it generally changes name once through the door. If you call foo(a), a becomes x once inside.
Type-unstable variables are useful sometimes, and :: already means different things in different contexts. On right-hand expressions, itâs a typeassert. On left hand side of assignments, in local statements, or struct fields, it restricts the type with converts.
Neither can be said to resemble argument annotations more. For example, is function f(x::Int) end supposed to resemble the call f(1::Int) or a variable restriction x::Int=1? So itâs free to do something different: dispatch.
macro looks at the signature of the function (that is all spelt out in code, no need to access any runtime information).
macro changes the code of the function to add some lines in the following format at the start of the function body name_parameter_1::type_parameter_1 = name_parameter_1âŚ
This is, what I thought initially was just a handy way to copy-paste the parameters of the function to the function body, in a way the parameter variables are now type-annotated with the same types they have in the parameter list.
Food for thought on that macro that enforces input type stability:
julia> function foo(x::AbstractFloat)
x::AbstractFloat = x
x = 2 # convert(AbstractFloat, 2)
x
end;
julia> typeof(1.5f0), typeof(foo(1.5f0))
(Float32, Float64)
julia> function bar(x::AbstractFloat)
x::typeof(x) = x
x = 2 # convert(typeof(x), 2)
x
end;
julia> typeof(1.5f0), typeof(bar(1.5f0))
(Float32, Float32)