"Variables do not have types, only values" - or maybe they do?

I’m a beginner so please forgive me if I ask something obvious. Here is a sentence from the beginning of the Types · The Julia Language documentation:

Only values, not variables, have types – variables are simply names bound to values.

When I first saw this, I quickly put together the following code:


julia> let
           println("One")
           x::Integer = 6
           println("Two")
           x = "Test"
           println("Three")
       end

I have tried to figure out what will happen when I execute it. This was my thinking:

  • The variable x is declared to hold values of Integer instances
  • In the let block, there is an assignment that tries to assign a String value to that variable.
  • The documentation stated that variables don’t have types, only values do.
  • So I would expect that Julia might not compile this block of code, because it contains an incompatible assignment. (Altough this would require that it knows the type of the value in advance. But in this case, this is attributable.)
  • From the other side: if Julia can compile the above code, then it must not throw an exception when the assigment happens. The assignment just re-assigns the name after all, and the name x does not have a type. (Remember: only values can have types, not names.)

I was very suprised, because this is what happened:

One
Two
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Integer
Closest candidates are:
  convert(::Type{T}, ::T) where T<:Number at number.jl:6
  convert(::Type{T}, ::Number) where T<:Number at number.jl:7
  convert(::Type{T}, ::Ptr) where T<:Integer at pointer.jl:23
  ...
Stacktrace:
 [1] top-level scope at none:5

Julia has compiled the code block before executing it, because “One” and “Two” were printed before the exception was thrown. I think it is clear that the code block was compiled at once, as a single compilation unit - the outermost let block must have been closed to form correct syntax, and also to define the scope of the variable x. The scope of x must have been determined before it was first assigned to. So the compilation succeded, the execution of the compiled code started and then a runtime error occured.

Julia called convert when she tried to convert the value "Test" to an Integer. It is true, that the first value assigned to x was an Integer, but that is the type of the value, not the “type of the variable”. There should be no such thing as “the type of the variables”, because “variables don’t have types”. Then why did she fail to assign a string to x? Where did the knowledge came from about x not being able to hold String values? In theory, this could not come from the variable x (wich is a variable, so it cannot have a type).

Either the documentation is not telling the whole truth, or maybe I’m not understanding something very fundamental about Julia. :slight_smile:

1 Like

In the Type Declarations subsection of the manual it states:

When appended to a variable on the left-hand side of an assignment, or as part of a local declaration, the :: operator means something a bit different: it declares the variable to always have the specified type, like a type declaration in a statically-typed language such as C. Every value assigned to the variable will be converted to the declared type using convert :

So when you do

x::Integer = 6

you’re declaring that the variable x can only be bound to values that have types that are subtypes of the asbstract type Integer. I think this is consistent with the statement

Only values, not variables, have types

Although, perhaps the bit of the manual I quoted above should be changed to read “it declares that any values bound to the specified variable must have the specified type.”

4 Likes

Thanks for the explanation. I understand that the documentation cannot specify every detail at the very beginning. There has to be some basic knowledge about the language before you can talk about more specific details.

So the whole truth is that variables (with the exception of global variables) can have a type, and this type information is used to convert any value if the value to be assigned does not match the declared type of the variable.

Well, that statement still disturbs me. It seems to be the exact opposite of the reality. :slight_smile:

I always know that statement is going to confuse someone that is reading carefully and I always try to add “normal variables”, “untyped variables” whenever I talk about it…

The point that line trying to make is that type is always the property of the object. There are of course ways to restrict the type of the variable and there are tools to help you do that but in the end it is still very different from other languages where the type of the variable is all what there is. (or variable is itself indistinguishable from objects). The difference between how you could understand it and what it really means is pretty subtle. People that are familiar with C++ or like should be able to figure out what it actually mean but I don’t really know of a way to make it clear and concise…


The statement also isn’ wrong though. Even if you declared the type of a variable, when you didn’t declare a concrete type the variable doesn’t really have a concrete type (if you remove the restriction of concrete than everything has type Any which isn’t very informative). It is also the case the case that the variable may NOT have a value in which case it’s type is basically Union{} in some sense.

2 Likes

Maybe the docs could instead phrase it as

Variables are simply names bound to values. As such, unless the type of a variable is constrained separately, only values, not variables, have types.

It’s just on the wording but even when the type of a variable is constrained it still doesn’t have a type in the same sens that an object has a type…

The type on the variable, or function argument, or function return type declaration, are more or less filters (with conversions). Sure they might have a property that is a type and they might only fit object of a certain type but that does not make them of be of that type.


Like if you have a box that can only hold books instead of papers, that box will be a box of books but the type of that box isn’t book. (And sure you can call it a book box but I think you ca see the fine difference of the type of the box being book and books being books…)

6 Likes

I think this might be the fundamental misunderstanding. Rather than thinking of names like x as being containers that hold values, you should think of them as labels that are attached to values. The statement x = foo() doesn’t put foo() “inside” x, it first evaluates foo() and then attaches the label x to the return value.

This also applies to y = x. Semantically this just attaches the label y to the same value that x is attached to (so now the value has 2 labels attached to it, i.e. both labels point to the same thing).

If later on you write y = 10 you’re taking the y label and moving it onto a new value, this time the value is 10. This is consistent with the above mental model if you think of all the possible integers as sitting together on a shelf, or in a block of memory somewhere, and you’re attaching the y label to one of them. In practice usually if you have x = 10 and y = 10 they don’t actually point to the same place in memory, but that’s an implementation detail.

The syntax x::Integer = 10 does two things - it says that x is a label that can only be applied to Integer objects, and it then attaches the label to the value 10. Later on when you try to apply the label to a String it throws an error because that label is Integer-only.

In other words, it’s not failing to assign a String to x, it’s failing to assign x to a String.

As a side note, this also explains why in Julia immutibility and copy-vs-reference behavior are tied together. If bar() returns a value that’s immutable, then the compiler is free to copy it as-needed, and the label semantics described above still hold, even though under-the-hood the labels might be referring to different places in memory.

6 Likes

Yes, I’m aware of that. I’m coming from Python where names are also just references, they don’t “contain” the values.

Then maybe we could say that variables can have “type constraints” but not “actual type”, only values have “actual types”.

I took a single sentence out of context. Also, this is an introductory sentence from a longer explanation. I did not want to tease. My goal was to understand what really happens, and why my example code threw a runtime error instead of a compilation error. I think I could understand why. :slight_smile:

Thank you for your help!

Oh yes, that was my mistake. :slight_smile: Of course, x is assigned to a value and not the other way.