In the last post we saw variables that can hold integers and floating point (decimal) numbers. Julia has other numeric types, and we'll see that these types are arranged in a hierarchy.
Integers and Floating Point Numbers¶
As review, here's how you store an integer into the variable x and a floating point number into the variable y
xxxxxxxxxx
x = 358
358
xxxxxxxxxx
y = 78.25155
78.25155
Complex Numbers¶
Julia has built-in support for complex numbers - we can create them, store them into variables, and perform calculations with them.
xxxxxxxxxx
# Here's a complex number with real part 3, and imaginary part 4
z1 = 3 + 4im
3 + 4im
xxxxxxxxxx
# The real part of that complex number is extracted like this:
real(z1)
3
xxxxxxxxxx
# ...and the imaginary part is extracted like this:
imag(z1)
4
xxxxxxxxxx
z2 = -12 + 5im
-12 + 5im
xxxxxxxxxx
z1 + z2
-9 + 9im
xxxxxxxxxx
z1 * z2
-56 - 33im
xxxxxxxxxx
z1 / z2
-0.09467455621301774 - 0.37278106508875736im
xxxxxxxxxx
abs(z1)
5.0
xxxxxxxxxx
angle(z1)
0.9272952180016122
xxxxxxxxxx
# Here's how you calculate the conjugate of a complex number
conj(z1)
3 - 4im
xxxxxxxxxx
# The product of a complex number a + bi and its conjugate a - bi
# Should be the real number a^2 + b^2
z1 * conj(z1)
25 + 0im
xxxxxxxxxx
# Note that we do NOT get automatic conversion to real numbers:
2im^2
-2 + 0im
xxxxxxxxxx
# The result of this calculation should be -1, but we get a rounding error:
ℯ^(π*im)
-1.0 + 1.2246467991473532e-16im
xxxxxxxxxx
# The number 1.2246467991473532e-16 is scientific notation and just means 1.2246467991473532 * 10^-16
# Not quite zero but awfully close!
Rational Numbers¶
Rational numbers are entered like this: 3//4 for the fraction 3/4.
xxxxxxxxxx
3//4
3//4
xxxxxxxxxx
# We can add, subtract, multiply, and divide rationals
3//4 + 9//15
27//20
xxxxxxxxxx
# Here's a finite harmonic series
1//1 + 1//2 + 1//3 + 1//4 + 1//5
137//60
xxxxxxxxxx
# They can be assigned to variables just like any other type of number
r1 = 3//4
3//4
xxxxxxxxxx
r1^2
9//16
xxxxxxxxxx
# Note that we do NOT get automatic conversion to integers!
4*r1
3//1
Numeric Type Hierarchy¶
These types, integer, real, complex, and rational, are called... numeric types. Imagine that.
xxxxxxxxxx
#=
As it goes, the type of value stored in a variable can change as our programs become more complex. This is
called "type instability."
If we're not sure what type of number is stored in a variable, use the typeof() function to figure this out.
=#
typeof(x)
Int64
xxxxxxxxxx
typeof(y)
Float64
xxxxxxxxxx
typeof(z1)
Complex{Int64}
xxxxxxxxxx
typeof(r1)
Rational{Int64}
The "64" in the results indicates how many bits are used to store that number. So, x is an integer and takes up 16 bits of memory, etc.
As it goes, these numeric types form a hierachy - a structure that looks like an upside-down tree - that our code can utilize within Julia. This doesn't sound too interesting in itself, but it will turn out to be extremely useful several chapters from now!
xxxxxxxxxx
# To see the parent type of Int64 in the hierarchy, do this:
supertype(Int64)
Signed
xxxxxxxxxx
# What does that mean? It means that Int64s can include a negative sign.
xxxxxxxxxx
# The parent type of Float64 is
supertype(Float64)
AbstractFloat
xxxxxxxxxx
# Let's repeat this:
supertype(AbstractFloat)
Real
xxxxxxxxxx
# And again
supertype(Real)
Number
xxxxxxxxxx
# And again!
supertype(Number)
Any
xxxxxxxxxx
# Is there anything above Any?
supertype(Any)
Any
There's nothing above Any, we've reached the top of the hierarchy. This is not only the top of the numerical type hierarchy, but the top of all Julia types, including non-numeric values like strings, booleans, etc.
If we were to repeat this process of repeatedly calling supertype on Int64, we will eventually get to Any. Same for Complex{Int64} and Rational{Int64}.
So supertype takes us up the type hierarchy. To see what is below a given type, use the subtypes function.
xxxxxxxxxx
subtypes(Any)
510-element Vector{Any}: AbstractArray AbstractChannel AbstractChar AbstractDict AbstractDisplay AbstractMatch AbstractPattern AbstractSet AbstractString Any Base.AbstractBroadcasted Base.AbstractCartesianIndex Base.AbstractCmd ⋮ Tuple Type TypeVar UndefInitializer Val Vararg VecElement VersionNumber WeakRef ZMQ.Context ZMQ.Socket ZMQ._Message
Wow, that's a lot of types, the majority of which we'll never directly use! Let's start from a type that's one step under Any, the Number type. BTW, Number would be in the list of subtypes of Any, but the output got truncated.
xxxxxxxxxx
subtypes(Number)
2-element Vector{Any}: Complex Real
That's interesting, there are two types under Number - and they are not arranged like mathematics tells us! As it goes, this is done for memory efficiency.
xxxxxxxxxx
# Let's continue down the Real branch:
subtypes(Real)
4-element Vector{Any}: AbstractFloat AbstractIrrational Integer Rational
xxxxxxxxxx
# And again
subtypes(Integer)
3-element Vector{Any}: Bool Signed Unsigned
xxxxxxxxxx
# Yes, booleans are stored as a numeric type.
subtypes(Signed)
6-element Vector{Any}: BigInt Int128 Int16 Int32 Int64 Int8
xxxxxxxxxx
# So there's all the different types under signed integers. If we try going down further...
subtypes(Int64)
Type[]
xxxxxxxxxx
# There's nothing below it!
There is much more to say about Julia type hierarchy, but that'll take us too far afield.
Type Inference¶
If you've studied languages like C or Java, the thing that should stand out is that we've not specified which types of values a variable can hold. This is mostly unnecessary, as the Julia compiler is very good at "type inference" - figuring out what type of value a variable can hold.
When the Julia compiler makes a mistake, that can lead to performance problems as well as our programs using more memory is necessary!
Here's a blatent example of how types can change:
xxxxxxxxxx
# Let's store an integer in a variable called p:
p = 5472
5472
xxxxxxxxxx
typeof(p)
Int64
xxxxxxxxxx
# Now let's make p hold string:
p = "Hello, World!"
"Hello, World!"
xxxxxxxxxx
typeof(p)
String
If the C or Java compilers would let us get away with making p hold a string, then they would surely complain when we made it hold an integer!
Here's a more subtle example of type instability:
xxxxxxxxxx
q = 50
50
xxxxxxxxxx
typeof(q)
Int64
xxxxxxxxxx
q = q + 2.0
52.0
xxxxxxxxxx
typeof(q)
Float64
We will see later how to specify what types of values a variable can hold.
Getting a List of Variables¶
A list of all variables and the types of values they contain is very convenient, especially in long notebooks or in the REPL. Here's how to get that list:
xxxxxxxxxx
varinfo()
name | size | summary |
---|---|---|
Base | Module | |
Core | Module | |
Main | Module | |
p | 21 bytes | 13-codeunit String |
q | 8 bytes | Float64 |
r1 | 16 bytes | Rational{Int64} |
x | 8 bytes | Int64 |
y | 8 bytes | Float64 |
z1 | 16 bytes | Complex{Int64} |
z2 | 16 bytes | Complex{Int64} |
The first three items on that list, Base, Core, and Main, are the "modules" (code libraries) that are automatically available to you when you code in Julia. We will ignore them for now, though we have implicitly used all of them.
Some IDEs, like Atom and VS Code, show this list as part of the user interface. Those IDEs are calling varinfo() in the background.
Also notice that the size of the values stored in the variables are reported in bytes instead of bits. There are 8 bits in a byte, and that's why a 64-bit integer (Int64) is shown to use 8 bytes.
Conclusion¶
We covered the following topics in this post:
- Complex and Rational values
- Determining the type of a value using the typeof() function
- Julia's type hierarchy, and the Any type at the top
- How to use supertype() and subtypes() to explore this hierarchy
- Type inference
- Type instability
- Using varinfo() to get a list of currently-defined variables.
Notice the language used in discussing types: a value has a type - and a variable takes on the type when that value is assigned to it.
No comments:
Post a Comment