How Julia works?
Type system
Until now, we have not used type annotations, but Julia has a rich type system. Julia is an optionally- and dynamically-typed programming language. That means that you can change the type of a variable...
a = 10
typeof(a)Int64a = true
typeof(a)Bool... and that type annotations are optional. You can use type annotations to:
- make a program more robust (type checking)
a::Bool # type assertiontruea::Int- optimize code by giving a hint to the compiler
- documment the code and use multiple dispatch
# function_name(arg::ArgumentType)::ReturnType = function_body
first_character(str::AbstractString)::Char = str[1]first_character (generic function with 1 method)character = first_character("ABC")'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)character = first_character(10_000)Type hierarchy
Any Julia object has a type that belongs to a fully connected type graph. There are abstract and concrete types. Concrete types are final, i.e. they cannot have subtypes, while abstract types can have multiple subtypes but only one supertype.
using JuliaForBioinformatics
show_type_tree(Number)Any
Number
Complex
Real
AbstractFloat
BigFloat
Float16
Float32
Float64
AbstractIrrational
Irrational
FixedPointNumbers.FixedPoint
FixedPointNumbers.Fixed
FixedPointNumbers.Normed
ForwardDiff.Dual
Integer
Bool
Signed
BigInt
Int128
Int16
Int32
Int64
Int8
Unsigned
UInt128
UInt16
UInt32
UInt64
UInt8
Rational
Ratios.SimpleRatio
StatsBase.TestStatIn Julia, all values are instances of the abstract type Any.
The functions supertype and subtypes are useful to navegate the type graph.
supertype(Real)Numbersubtypes(Real)You can use isa to test if an object is of a given type
isa("I'm a string", String)trueAnd the subtype operator <: to test if a type is a subtype of another
String <: AbstractStringtrueYou can also use Union of types, for example, if the possible types don't share a meaningful supertype
String <: Union{AbstractString, AbstractChar}trueMultiple dispatch
We can define multiple methods for a function by using different method signatures by indicating the argument types using :: or <:.
For example we are going to define 3 methods for the function say_my_type:
say_my_type(x) = println(x, " is a ", typeof(x))
# say_my_type(x) is the same that say_my_type(x::Any)
say_my_type(x::Real) = println(x, " is a Real number of type ", typeof(x))
say_my_type(x::Float64) = println(x, " is a Float64 number")say_my_type (generic function with 3 methods)When the function is called, Julia selects the method with the most specific method signature.
say_my_type('A') # 'A' is a Char, a subtype of Any
say_my_type(2) # 2 is an Int, a subtype of Real
say_my_type(2.0)A is a Char
2 is a Real number of type Int64
2.0 is a Float64 numbersay_my_type(x::Real) can also be written using the where keyword as
say_my_type(x::T) where {T <: Real} = println(x, " is a Real number of type ", T)say_my_type (generic function with 3 methods)methods(say_my_type)3 methods for generic function say_my_type:- say_my_type(x::Float64) in Main.ex-04_HowJuliaWorks at none:1
- say_my_type{T<:Real}(x::T) in Main.ex-04_HowJuliaWorks at none:1
- say_my_type(x) in Main.ex-04_HowJuliaWorks at none:1
Parametric types
Julia types can have parameters. We have already used parametric types, one of them is Array:
three_d_array = zeros(Int, 4, 3, 2)4×3×2 Array{Int64,3}:
[:, :, 1] =
0 0 0
0 0 0
0 0 0
0 0 0
[:, :, 2] =
0 0 0
0 0 0
0 0 0
0 0 0typeof(three_d_array)Array{Int64,3}Julia Arrays take two parameters, the type of the elements stored in the array and the array dimensions.
This allows to write specific methods depending on those parameters
say_my_type(x::Array{T, 1}) where {T} = println(x, " is vector with ", T, " elements")
say_my_type(x::Array{T, 2}) where {T} = println(x, " is matrix with ", T, " elements")say_my_type (generic function with 5 methods)say_my_type(Rational[0.5, 1, 1.5])
say_my_type(Float64[1 3 5; 2 4 6])Rational[1//2, 1//1, 3//2] is vector with Rational elements
[1.0 3.0 5.0; 2.0 4.0 6.0] is matrix with Float64 elementsExercise 1
Add a method to say_my_type that prints the number of unique values of an array of characters or strings and its dimensions. Hint: You can use the unique function.
# ... println(x, " is a text array with ... dimensions and ... unique values of type ...That means that the function call:
test_array = ['a', 'b', 'b']
say_my_type(test_array)Should print something like:
['a', 'b', 'b'] is a text array with 1 dimensions and 2 unique values of type CharWhich method is being used?
You can use the @which macro to ask Julia which method is being used
@which say_my_type(2 + 0im)@which say_my_type(2.0)Julia compiler
Julia uses Just-in-time (JIT) compilation to achieve close to C performance. After selecting the most specific method, Julia (generally) compiles the method for the particular argument types.
For this reason, the first time a function is called, it is compiled (slow). If you call the same function a second time with the same argument types, it will use the already compiled code (fast).
@time sum(1:10_000)50005000@time sum(1:10_000)50005000While compilation times can be annoying sometimes, this mechanism allows Julia generality, composability and its capacity to generate efficient code for user-defined types. That's mean that you do not need to use built-in types or functions or to code some parts in C/Fortran to get a good performance like in other high-level languages.
Also, Julia represents its own code as a Julia data structure. This allows a program to transform and generate its own code, using macros and generated functions, for example, and powerful reflection capabilities to explore the internals of a program. You can read the metaprogramming section of the manual to learn more about this topic.
##?@elapsedmacroexpand(Main, :(@elapsed sum(1:10_000)))quote
#= util.jl:212 =#
local #64#t0 = (Base.time_ns)()
#= util.jl:213 =#
local #65#val = sum(1:10000)
#= util.jl:214 =#
((Base.time_ns)() - #64#t0) / 1.0e9
end
@which 1.0 + 2.0@which 1 + 2@code_lowered 1.0 + 2.0@code_typed 1.0 + 2.0@code_llvm 1.0 + 2.0@code_native 1.0 + 2.0Julia can do extra optimizations inside functions, e.g. constant propagation:
f() = 1.0 + 2.0
@code_typed f()Exercise 2
Modify the sum_numbers function to make it type stable by using zero and eltype.
To avoid performance issues for using a global variable we are going to define it as a constant using the const keyword.
const rand_matrix = rand(4, 4)4×4 Array{Float64,2}:
0.172628 0.911579 0.144012 0.894589
0.768832 0.295764 0.748298 0.267925
0.668007 0.178661 0.557616 0.748756
0.290388 0.176055 0.374053 0.42154 function sum_numbers(numbers)
total = 0
for value in numbers
total += value
end
total
endsum_numbers (generic function with 1 method)Check that you don't have any warning in the output of @code_warntype:
@code_warntype sum_numbers(rand_matrix)This page was generated using Literate.jl.