Julia: Tutorial & Code-Collection¶

Source: https://github.com/markomlikota/CodingSoftware

MIT License, © Marko Mlikota, https://markomlikota.github.io

Coding Advice¶


General Advice¶

  • Before you start coding, be clear on what you want to achieve and how to go about it (roughly).

  • Divide program into (many small) functions. This is easier to reuse, to test, and it (often) runs faster.

  • For standard problems (e.g. interpolation, kernel density estimation, etc.), before you do it yourself, see whether there is a library that does it.

  • For bigger projects, use Git to keep track of version changes.

  • If you find yourself creating a newer version of some file, leave the old version's name as-is and attach a version-suffix (e.g. _v3) to the filename of the new version. This way there is a unique filename for every new version, and old versions can just be discarded in an "OldFiles"-folder and re-taken if needed without creating naming problems (which would arise if you deleted the version extension for the currently best version).

  • Think about optimization only once the code runs. And ask yourself whether an optimization (making code faster, graphs prettier, etc.) is really needed.


Code Structure & Naming of Objects¶

  • Clearly structure your code using comments, lines and spaces to create sections, subsections, etc. This will make it much more readable (for your future self and for others).

  • At the top of your code, include a header/preamble with your name, contact information (email/website), the purpose of the code (title and a short description)

  • Start your code with some preliminary sections where you specify options, load all packages necessary to run the script, define paths, and load external code.

  • Adopt a naming convention, e.g.

    • use the so-called lowerCamelCase

    • start binary variables with "is" or "include" or other verb (e.g. isUtilityPositive or includeLimits)

    • start functions with f (e.g. fCombineStrings)

    • start object-names with v, a, m, i for vector, array, matrix, iterator, string (and combine, if necessary; e.g. vsVars is vector of strings

See https://github.com/markomlikota/CodingSoftware for template .jl- and .jmd-files.


Julia-Specific Advice¶

In [1]:
# As discussed above, Julia is a just-in-time-compiler;
# the first time a code is run, it optimizes its computations 
# so that the code is executed faster in subsequent calls
# (this holds for codes that are literally executed several times,
# but also for loops and similarly "repetitive" codes.)

# You can help Julia optimize the code in several ways.
In [2]:
# Constants and Global vs Local Variables

# Declare variables whose value will not change
# during the current Julia-session as constants: 
# e.g.
const δ = 5 
# One can, in principle, redefine a constant, 
# but you are advised not to do so:
δ = 3
WARNING: redefinition of constant Main.δ. This may fail, cause incorrect answers, or produce other errors.
Out[2]:
3
In [3]:
# Type-Stability

# Avoid changing the type of a variable;
# e.g. given
x = 1.5
# redefine x as 
x = 2.0
# rather than 
x = 2

# Let your functions' output be always of the same type,
# regardless of the type of the input supplied to them.
# e.g. write
fMyFun(x) = 2.0 * x
# rather than
fMyFun(x) = 2 * x
# (Former always returns float, 
# while latter returns output of same type as x.) 

# Ideally, also let your functions always take inputs of the same type,
# though this is, apparently, less important.
# e.g. write
fMyFun(x::Float64) = 2.0 * x
# and call 
fMyFun(2.0)
# or 
fMyFun(Float64(2))
# rather than letting, as above, fMyFun(x) take x of any type.
Out[3]:
4.0
In [4]:
# Pre-allocate outputs:
# e.g. instead of creating an object (e.g. a matrix) every time anew (say in a loop),
# create it once (e.g. before the loop) and then just fill it up with different values.
# This optimizes your code in general, 
# and even more so if the object stays of the same, specific type.
In [5]:
# Use @. in functions that perform vectorized operations on long vectors:
# e.g. this
fMyFun1(x) = @. 3x^2 + 4x^3 + 4
# is faster than this
fMyFun2(x) = 3x.^2 + 4x.^3 .+ 4

vx = rand(10000)
@time fMyFun1(vx)
@time fMyFun2(vx)
  0.128975 seconds (386.11 k allocations: 26.497 MiB, 99.96% compilation time)
  0.274181 seconds (350.97 k allocations: 24.391 MiB, 13.82% gc time, 99.97% compilation time)
Out[5]:
10000-element Vector{Float64}:
 10.496584532958767
  6.439532126827355
  4.001113742676998
  5.0568304912004844
 10.886869913279174
  5.9614361363542185
  4.516996449742614
 10.968299664943107
  4.553604952233805
  9.783594719420094
  4.803344608172253
  7.4565479830390675
  4.28177830647821
  ⋮
  8.57113519474725
  7.658954961238093
  6.285528900523082
  4.459668564411588
  8.456898753085628
  4.352197745895103
  4.1973337794137935
  4.785647651358424
  9.103117708280328
  4.518020819202778
  6.9489950634368
  4.119407764087473
In [6]:
# Consider using @views when accessing parts of a vector or a matrix.
# When you use e.g. mA[1:50,41:70], this creates a copy of this "slice" of mA.
# This is all right if you use this slice in several operations.
# However, if you do only a few operations,
# it is better to write "@views" in front of these operations,
# which avoids storing a copy of the slice.
# e.g. for
mA = rand(100,100)
# write 
@views mA[1:50,41:70]
# instead of
mA[1:50,41:70]

# compare time needed for these two methods of accessing:
@time @views mA[1:50,41:70]
@time mA[1:50,41:70];

# e.g. when using the slice in a function:
@time @views sum(mA[1:50,41:70])
@time sum(mA[1:50,41:70]);
  0.000006 seconds (1 allocation: 64 bytes)
  0.000015 seconds (1 allocation: 11.875 KiB)
  0.051160 seconds (92.54 k allocations: 6.315 MiB, 13.65% gc time, 99.88% compilation time)
  0.041072 seconds (28.68 k allocations: 1.965 MiB, 99.89% compilation time)