How to re-define a variable completely?

When I define a wrong (or unuseful) type to a variable declaration I get an error as expected:

my_var::Int64 = 3.2

However, when I continue to work in the REPL, it is impossible to re-define the variable:

my_var = 3.2

It seems, that the variable type is stored in the REPL. How can I clear or redefine the type without restarting the kernel?

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.

3 Likes

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).

5 Likes

Yes, but is that what should not be allowed. That variable name is already type signed in function’s argument.

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.

3 Likes

Also, note that your example given an InexactError but the below does not:

julia> function f()
         a::Int = 1
         a = 2.0
         return a
       end
f (generic function with 1 method)

julia> f()
2

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.

1 Like

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.

2 Likes

A typed global variable reduces the set of objects it can be rebound to. This is a softer version of const. Are you also opposed to const?

1 Like

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 :wink: 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:

julia> macroexpand(Main, :(@code_warntype sqrt(2)))
:(InteractiveUtils.code_warntype(sqrt, (Base.typesof)(2)))
1 Like

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.

Just to clarify, my idea of a macro would be:

  1. macro looks at the signature of the function (that is all spelt out in code, no need to access any runtime information).
  2. 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)