Julia DataTypes

In this article, we’ll discuss the various datatypes offered by Julia. If you want to read about Julia’s introduction and its installation then read our first article A Gentle Introduction to Julia.

Variable Names

Much like in Python, there are few constraints on variable names in Julia. There are two notable differences though. For one, as Julia doesn’t have to worry about backward compatibility, the set of ‘alphabets’ it draws upon is much larger. That is to say, it has full support for UTF 8 encoded Unicode.


Julia> Ω=" "
" "

Secondly, it doesn’t discriminate between variables based on their names. For example, the following variables will be treated the same by Julia.

Julia> _PEP8="this variable is meant for internal usage only"
"this variable is meant for internal usage only"


Julia> PEP8="even though one can work around it"
PEP8="even though one can work around it"

Following is an exhaustive list of all the words that Julia(1.5.2 reserves for itself, i.e. doesn’t allow for use as variable names:

baremoduledofinallylestruct
beginelseforlocaltrue
breakelseiffunctionmacrotry
catchendglobalmoduleusing
constexportifquotewhile
continuefalseimportreturn

Although there are a few other words like where that shouldn’t be used as variable names (even though it’s syntactically correct). And in case you want a near exhaustive definition of what is allowed outside the above listing, here’s what the documentation says:

Variable names must begin with a letter A Z or a-z), underscore, or a subset of Unicode code points greater than 00A0; in particular, Unicode character categories Lu/Ll/Lt/Lm/Lo/Nl (letters), Sc/So (currency and other symbols), and a few other letter-like characters (e.g. a subset of the Sm math symbols) are allowed. Subsequent characters may also include! and digits 0 9 and other characters in categories Nd/No), as well as other Unicode code points: diacritics and other modifying marks (categories Mn/Mc/Me/Sk), some punctuation connectors (category Pc), primes, and a few other characters.

Do note that Julia, being dynamically typed, doesn’t attach any type to variables, only to values. Without further ado, let’s serve today’s main course.

Primitive Types

While there are many different interpretations of type theory, giving rise to various type system implementations, in practice, numeric computation is done in two ways. There’s the OG way of choosing a memory size along with fixing a representation, this, for example, is the way C implements int and char . This is great for speed, but becomes cumbersome when doing more complex math. So, more scientifically inclined languages use infinite-precision arithmetic, Mathematica falls into this camp. Infinite precision, by the way is attained by storing abstract representation as opposed to numeric ones.

Julia, as usual, when given the choice between these two popular routes, chooses both. It has primitive numeric types as well as

There are 14 numeric primitive types in total. There are five sizes of integer types, from the 8-bit

signed Int8 to the 128-bit unsigned UInt128 , with the 16,32 and 64-bit variants in between, for a total of 10. And there are also the standard half,single and double precision floating point types: Float16 , Float32 and Float64 respectively. Finally Bool represents the Boolean type, which can be false aka 0 or true (aka 1 ). When declaring a new number, Julia infers the appropriate type by first factoring in the system architecture, and then going for least size.

For example, on a 32-bit system the default assigned types will be as follows:

 1 #Int32 
-1 #Int32
0.3 #Float32

Whereas, on a 64-bit system, the types will all be the 64-bit variants


1 #Int64 
-1 #Int64
0.3 #Float64

And unsigned integer types are always denoted in hexadecimals, being allocated the least possible size with regards to the number of digits ( no concerns for architecture).

0x42 #UInt8 and equals 42

0x00042 #UInt32 and equals 42

0b100 #UInt8 and equals 4

0o100 #UInt8 and equals 40

This again is extremely handy, as we generally use unsigned integers as constants, and looking at bigger constants is much easier in hexadecimal. Similarly, the other common operations we do on unsigned integers are bitwise operations, which are again much simpler to double-check in binary and octal.

Being dynamic, another convenience is offered by typemax and typemin functions, which help us avoid buffer-overflows by letting us keep track of the capacity of various primitive integer types. For readability, Julia aliases UInt and Int to the system’s native integer types. And as I’m on a 64-bit system.

Julia> typemin(Int), typemax(Int) 

(-9223372036854775808, 9223372036854775807) 

In case a buffer overflow is indeed imminent, Julia uses wraps the value around as if dealing in a modulo-typemax system.

x=0b10 #Set x to 2

typeof(x) # Confirm it's type to be UInt8

x=x+typemax(UInt8) # Topple it over to get to 1 via additio

Floating-point numbers are quite vanilla as well. They are used for division, and become infinity (or – infinity) on division by zero.

1/0	#Inf

-1/0    #-Inf

Oh, and yes, infinity comes in different sizes too, no not different cardinalities, different word sizes. And you can convert between different floats as follows

.1	#Float64 1.1

Float16(1.1)	#Float16 1.1

Float16(1.1)/0#=Inf16

But, of course, division by 0 of 0 is absolutely unacceptable. Each line in the following code-cell outputs NaN , which isn’t a type of bread, but also isn’t equal to any number either.

0/0

Inf - Inf16

Inf - Inf

Okay, now you know about integers and fractions in Julia. So, a simple exercise for you is to check what happens if you use typemax on float.

Now for the fun stuff, arbitrary precision arithmetic! Now, we’re not going to go into the details of the particularities of how Julia implements multiple dispatching, but we will tell you how you can leverage it.

Julia has BigInt and BigFloat for the so-called big-num arithmetic, which is just a fancy way of saying that no matter how big the quantity, or the number of decimal places requested by the user, we’ll calculate it exactly.

BigInt(typemax(Int128)) + 1 #Will give us a positive integer, which is out of range of Int128 
BigFloat(5.0^42)/7 #Similarly gives us an extreme number of digits after the decimal

But this isn’t arbitrary yet, arbitrary is when we get to define how much too much is. And to do that, we use

tprecision(4);BigFloat(1/7) #0.141

setprecision(40);BigFloat(1/7) #0.14285714285711

Real and Complex Numbers

Despite the aforementioned functionality, most often, instead of using BigInt or BigFloat , you’ll be utilizing the native rational (or complex) numbers.

14//28 #Is a rational number, and as such, is reduced to the simplest form 1//2 
Float(1//2) #Will give 0.5

im # represents the complex number i

im^2 # gives -1+0im as expected

Strings and characters

The traditional Char type is a 32-bit primitive type storing a Unicode character in the UTF 8 encoding. And String builds upon that to provide the robust string operations of Julia. This supports markdown and LaTeX in the fullest, and is hence quite intricate in itself. Alas, we shan’t dwell on the complexities here.

Type System

Abstract Types

Again, given the choice between being statically or dynamically typed, Julia chooses both. The Julia programmer is never forced to mention types, but, if they so choose to do that, it will be used almost as if Julia were a strongly typed language, i.e. enforce the type assertion, and result in a speed-up via less dispatch calculation. To this extent, Julia adopts the Haskell notation for inheritance, :: .

Now, for some notes from the docs: All values in Julia are objects, with a single type, which belongs to a fully-connected graph of types. Abstract as well as concrete types can be parametrized by other types, even symbols!

Type assertions throw an exception when types don’t match, while type annotations try to convert when the types don’t match.

foo::Int	#This is a type assertion

foo::Int=56 #This is a type annotation

local foo::Int # This is also a type assertion

Then, we of course have the abstract types, declared via one of the (reserved) keywords abstract type .

abstract type foo end #Delare an abstract type foo

abstract type foo <: bar end #Delare an abstract type foo as a subtype of bar

Note that in Haskell fashion, the default super-type is Any. Some built-in abstract types of Julia are Numbers, which is a supertype of Real. Real, representing real numbers, is a supertype of Integer and AbstractFloat. Moreover, <: helps you check if a type is a subtype of another.

Integer<:Real #true

Any<:Real	#false

You can check this way, that all the primitive types we started of with, are subtypes of their respective abstract types. In fact, you can even declare your own primitive types!

primitive type Float16 <: AbstractFloat 16 end #This is the actual definition of Float16

primitive type Float256 <: AbstractFloat 256 end #And this is a new primitive, a floating point number of 256 bits.

Composite Types

This is where things really start diverging from languages like Python, as Julia’s inspirations lie more from purer languages like Haskell and Smalltalk. So, because all values are objects in Julia, and the method called for a function is determined by all of the passed-in types (rather than just the first one) functions are not packed up with their operated object.

Composite types are defined via the struct keyword:

struct Foo1	#defines the composite object named Foo, via its default constructor

s::String

Ω::Int

foobar::Char

end

foo = Foo("Hi", 23, ' ') #assigns an instance of Foo to the variable foo

You can query the fields of a composite type via fieldnames . And the usual dot-notation can be used to access them.

fieldnames(Foo) #Will give (:Ω,: ,:foobar)
foo.Ω #Hi

Beware, struct defines an immutable (composite) type. In order to define mutable (composite) types, you use a mutable struct.

mutable struct moo	#This is exactly the same as Foo, just, it can be changed

Ω::Int
foobar::Char

end

Do note that immutability is preferable due to speed. And also that if you declare no fields, you get what is called a singleton type, that can naturally have only a unique instance.

Further Notes

Internally, all aforementioned types are represented as instances of DataType. This further facilitates higher-order operations on already declared datatypes. One important one is Union, which is very similar to Haskell’s Maybe, where it amalgamates multiple types by having them be one of many.

4::Union{Nothing,Int,UInt} #Is ok, as 4 is an Int

"4"::Union{Nothing,Int,UInt} #Throws an error due to "4" not being of a type in the Union

There are more complex type operations in Julia, but we don’t mention them here for their lack of usage in usual Data Science as well as them currently still being in development.

References

Leave a Reply

Your email address will not be published.