How to re-define a variable completely?

yeah that’s pretty easy to do:

using ExprTools

macro stable_argtypes(fdef)
    d = splitdef(fdef)
    args = get!(d, :args, [])
    arg_asserts = Expr(:block)
    foreach(args) do arg
        if Base.isexpr(arg, :(::), 2)
            name, type = arg.args
            push!(arg_asserts.args, :($name :: $type = $name))
        end
    end
    old_body = get!(d, :body, Expr(:block))
    d[:body] = Expr(:block, arg_asserts, old_body)
    esc(combinedef(d))
end

and now

julia> @stable_argtypes function f(x::Int)
           x += 1.5
           x
       end
f (generic function with 1 method)

julia> f(1)
ERROR: InexactError: Int64(2.5)
Stacktrace:
 [1] Int64
   @ ./float.jl:900 [inlined]
 [2] convert
   @ ./number.jl:7 [inlined]
 [3] f
   @ ./REPL[5]:2 [inlined]
 [4] f(x::Int64)
   @ Main ./none:0
 [5] top-level scope
   @ REPL[6]:1
3 Likes

EDIT: IGNORE THE ANSWER BELOW, I HAVE MISUNDERSTOOD WHAT THE MANUAL MEANT.

This is kinda specific behaviour that comes from Base.convert:

If T is a AbstractFloat type, then it will return the closest value to x representable by T.

To be honest, I find quite unfortunate that convert (which is used by the language itself in some non-intuitive situations like this) works for abstract types. This shows that this trick can have cause the opposite of the desired effect, instead of helping the code to be type stable, it becomes type-unstable because the concrete type returned by convert(AbstractFloat, x) will depend on the value of x.

Thank you very much! Unfortunately I have dabbled very little in actual macro writing and it would be hard for me to do it even if I see it is possible.

As said before, you can’t undo global variables like that. Interestingly, the variable is still technically undefined because it’s not assigned, even though it exists and has its type restricted:

julia> my_var::Int64 = 3.2
ERROR: InexactError: Int64(3.2)
...
julia> my_var
ERROR: UndefVarError: my_var not defined

julia> my_var = 1.5
ERROR: InexactError: Int64(1.5)
...

So your global scope is stuck with a weird variable, and now you have to kill the whole REPL…is what people would’ve said before v1.9. Now you can change what module the REPL evals in, you’re no longer stuck in Main. I don’t do this often because I already have the habit of trying things out in throwaway functions and let blocks for local scope behavior, and I still think that’s much better than bailing on a global scope. But if you want to switch out of Main and don’t want to reload all the other modules when you reimport them, this is how you can do it. Just have to be aware that if those modules were defined in Main then you will still have to import them from Main.

No, convert(AbstractFloat, ::Int64) is always a Float64, it doesn’t depend on the value of x.

IMO there’s nothing really wrong with @Benny’s example here. The user asked that the input type be AbstractFloat, so that implies any AbstractFloat should be fine to convert to.

Without the implicit convert steps, the resulting type of x would have ended up being Int64 afterall.

Sorry, had to leave and meanwhile lots of water passed under this bridge but let me still add that my vision is more like this. If that value wants to change clothing at its own will, than it will have to enter naked. If it enters in uniform than it must keep it all time. This still looks pretty dynamic to me. That way we could have a:

function foo(x::police=empty_police_type, kw…)
	if isempty(x) && isa(var_in_kw, police)		# Here ‘var_in_kw’ may very likely be a Any
		x = var_in_kw				# Uf, got rid of that Any
	end
end

as it is, x will become a Any too. I do lots of parsing from values in a Dict{:Symbol, Any} and getting rid of those Any’s, even when I do know their true types, is a nightmare.

Then how would multiple dispatch be implemented? I could very well need a foo(x::Int) method where x could be reassigned a floating point value, and I cannot refactor as a foo(x::Real) because that is already a separate method handling calls like foo(1.5). Your design proposal isn’t crazy, it feels like a statically typed language but it has controllable dynamic typing via abstract type annotations. But multiple dispatch needs input type subsets somewhere, and argument annotations serve that purpose already. Earlier I said it’s not like either typeassert or convert kinds of type declarations, but it’s a bit more like the former: foo(1.5::Int) fails but foo(1.5::Real) runs, which is a really bad kind of method dispatch.

I have edit my answer. I have, in fact, misunderstood what the manual meant. The manual does not make mention to convert(AbstractFloat, x), and it is not clear what behavior should it have, if any. What the manual referred to was any convert(T, x) in which T <: AbstractFloat (and probably only cases in which T is a concrete type). Also, I said x where x is not obligatorily of type Int, but I did not really find a case in which the result type depended on the value (not the type) of the input.

1 Like

Hmm, you’d do a xf = Float64(x) and use it hence-after, no?
I don’t see, but ofc may be my problem, where what I’m saying would interfere with multiple dispatch. Annotated and non-annotated input variables would work as they do, just that, once a police through the door, always a police inside.

No. In a truly dynamically typed language, variables have to be able to accommodate unpredictable types from function calls. I said “could be”, not “will be” for that reason. Maybe an example will help you see it better. In your design proposal, a fully flexible variable would be annotated ::Any

#=
I won't bother implementing this, but just know this
can return any type randomly
=#
function updatex end

function foo(x::Any)
  for i in 1:3
    x = updatex(x)
    println(x)
  end
end

foo(1)

Seems good so far, until I want the input type to change what is printed. In current Julia, I can do this with argument annotations:

function foo(x::Integer)
  for i in 1:3
    x = updatex(x)
    println("Started as an Integer, now we're", x)
  end
end

function foo(x::AbstractFloat)
  for i in 1:3
    x = updatex(x)
    println("Floated down to a ", x)
  end
end

foo(1)
foo(1.5)

But that won’t compile in your design proposal, updatex(x) would be inferred as a ::Any. But like you said, I don’t need to reassign x at all, I could just make another variable x2… but what type do I annotate it with? I often don’t know in advance what exact types a function call may return, even if I’m sure it’s type-stable. So I won’t annotate it at all, leave it as an implicit x2::Any = x.

But you see, we’re really back where we started: we selected a method using the input type, and we work with unpredictable types starting from there. The difference is we can’t reassign the argument variable and have to make an extra local variable.

In my design proposal (well, it isn’t really a proposal because Julia is not going to change) x = updatex(x) would error every time update(x) did not return an Integer because x had to retain its input type.

But

function foo(x::AbstractfFloat)
  for i in 1:3
    x = updatex(x)

should allow updatex(x) returning Integers that would be promoted to float.

I think what Joaquim says that the language could behave differently, more akin to a type-annotated language. I think he is correct, it could. It just doesn’t, and this does not seem something fundamental for the majority of Julia programmers, because we seek type-stability anyways by following some patterns and using some linter/code analysis tools.

But in many scenarios it is not unreasonable to expect the languages to behave like that, and people like statically typed languages for this reason. What I wander is is there some possible macro that, given some syntax (new or existing), give the same level of type certainty a statically declared language provides. For example, if one writes something like:

function foo(x)
    local x::Int
    local y::Int
    x = y + 1
    z = 2y
    return z
end

From what I see, if x and y are assigned to values not convertible to Ints, this will error. Good. If there was an analysis that warned the user that he/she “forgot” to declare the type of z, then one would be almost there (or totally there?) in terms of statically typed analysis. It wouldn’t be bad to have some tool providing that kind of analysis and guarantees.

2 Likes

Thanks, I think it’s almost that. The difference is what I’m saying is that the above example should apply when input x is annotated. i.e. function foo(x::Int)

Which is awful for a dynamically typed language, and also not what a statically typed language would do. A statically typed language restricts to concrete types so it would be more akin to x::typeof(x) = x I said earlier, not x::Integer = x. The latter isn’t necessarily type-stable, either, there are many Integer types.

1 Like

Yes, it could be for the annotated input and output foo(x::Int)::Int, for example. But that is very limited in terms of type stability, because inside the function one would still be able to define other variables that change type at will. Thus, I was going a little bit further and requiring all variables to have their types explicitly declared. If that was combined with generic inputs, like:

function foo(x::T)::T where {T<:Real}
    local x::T, y::T, z::T
    y = x + one(T)
    z = x + y
    return z
end

and something/some tool can throw a warning if any of the variables inside foo is not declared or may assume a wrong type, that would be nice.

But why we do not have that already? Because, I think, that is not much different from having simply

function foo(x::T) where {T<:Real}
    y = x + one(T)
    z = x + y
    return z
end

and use JET, Cthulhu or @code_warntype to check for type instabilities for a specific set of inputs, without the variable declaration boilerplate.

Tangentially relevant: