Julia
Tags :: Language
Project managment
Initalizing a project
Create the basic source tree and toml from the repl in pkg mode:
pkg > generate MyProject
The created source tree will be
MyProject/
├── Project.toml
└── src
└── MyProject.jl
Both Project.toml
and Manifest.toml
are central to a project, but
with some key differences. - Project.toml
can be edited manually -
Manifest.toml
is generated and maintained by Pkg and should never be
edited manually. ## Activating project environment Two ways to activate
a project environment, from Pkg repl
pkg > activate .
or from the shell
julia --project=.
Adding dependencies
After activating project, you can add dependencies from the Pkg repl
with add <pkg>
, e.g.
(MyProject) pkg > add Plots
The Project.toml
will be updated to include the dependency name and
it’s hash code.
Testing
Packages can additionally be tested (provided they have tests) after install as so:
pkg> test ArchGDAL
and a nice summary will be output.
Loading
It is common to load in the full namespace of a package with the using
keyword:
using ArchGDAL
however, you can also assign the name space to a variable
const AG = ArchGDAL
Functions
All arguments to functions are passed by sharing (i.e. w/ pointers). The convention for naming a function which will mutate or destroy the value of one or more of its arguments is to end it with !
(e.g. sort
vs sort!
).
Callees must make explicit copies to ensure that they don’t modify inputs that they don’t intend to change. Many non-mutating functions are implemented by calling a function of the same name with an added !
at the end on an explicit copy of the input, and returning that copy.
simple function notation;
function f(x,y)
x + y
end
Multi-dim Arrays
Julia provide first-class array implementation. The array library in Julia is implemented almost completely in Julia, and derives performance from the compiler. Given this, it is possible to define custom array types by inheriting from AbstractArray
(more details).
Zero dimensional arrays are allowed, and in general an array can contain objects of type Any
.
Julia is unique in that it does not expect programs to be written in a vectorized style for performance. The Julia compiler uses type type inference and generates optimized code for scalar array indexing. This allows for readable code without sacrificing performance and using less memory.
Types
Type system is dynamic, but has optional static typing. The default behavior when types are omitted is to allow values to be any type. Adding annotations serves three primary purposes: 1. Take advantage of multi-dispatch system 2. Improve readability 3. Catch programmer errors
The type system is dynamic, nominative and parametric. Generic types can be parameterized, and the hierarchical relationships between types are explicitly declared rather than implied compatible structure.
A distinctive feature of the type system is that concrete types may not subtype each other: all concrete types are final and may only have abstract types as their supertypes. Being able to inherit behavior is much more important than being able to inherit structure.
Other high-level aspects:
- There is no division between object and non-object values.
- All values in Julia are true objects having a type that belongs to a single, fully connected type graph, all nodes of which are equally first-class as types.
- There is no meaningful concept of a “compile-time type”.
- The only type a value has is its actual type when the program is running.
- Only values, not variables, have types. Variables are simply names bound to values.
- Both abstract and concrete types can be parameterized by other types. They can also be parameterized by symbols, by values of any type for which
isbits
return true, and also by tuples thereof. Type parameters may be omitted when they do not need to be reference or restricted.
Declarations
Attach annoations to expressions and variables with ::
operator. When
appended to an expression computing a value, it is read as “is an
instance of”. It can be used anywhere to assert that the value on the
left is an instance of the type on the right.
julia> (1+2)::AbstractFloat
# ERROR: TypeError: in typeassert, expected AbstractFloat, got a value of type Int64
julia> (1+2)::Int
# 3
Abstract Types
These cannot be instantiated and only serve as nodes in the type graph, describing sets of related contrete types which are their descendents. They form the conceptual hierarchy of the type system.
Abstract types are declared with the abstract type
keyword. The
general syntax for declaring are
abstract type «name» end
abstract type «name» <: «supertype» end
the name can optionally be followed by <:
and an already existing type
indicating that the newly declared abstract type is a subtype of this
parent type.
Composite Types
Also known as records, structs, or objects. It is a collection of names fields, an instance of which can be treated as a single value. All values are objects, but functions are not bundled with the objects they operate on.
Composite types are defined with struct
and a block of fieldnames
which are optionally annotated.
struct Foo
bar
baz::Int
qux::Float64
end
Use fieldnames
to get a list of field names.
Composite types with struct
are immutable and can not be modified
after construction. This has several advantages:
- More efficient. Some structs can be packed efficiently into arrays or the compiler may be able to avoid allocating immutable objects entirely.
- It is not possible to violate the invariants provided by the types constructors.
- Code using immutable objects can be easier to reason about. The fields of an immutable object can contain mutable objects, but the fields cannot be changed to point to different objects.
When mutability is required, mutable composite objects can be declared
with the keyword mutable struct
.
Package Development
Since the Project.toml
file src/*.jl
files are sufficient for
determining a package, packages are modules with their own environment.
PkgTemplates
using PkgTemplates
template = Template(;
user = "GithubUserName", # github user name
authors = ["Author1", "Author2"], # list of authors
dir = "/Path/To/Dir/", # dir in which the package will
# be created
julia = v"1.7", # compat version of Julia
plugins = [
!CompatHelper, # disable CompatHelper
!TagBot, # disable TagBot
Readme(; inline_badges = true), # added readme file with badges
Tests(; project = true), # added Project.toml file for
# unit tests
Git(; manifest = false), # add manifest.toml to
#.gitignore
License(; name = "MIT") # addedMIT licence
],
)
Do not forget to change user
, authors
and dir
.
template("PackageName")
For naming conventions, see the
official package naming guidelines. Finally, create the folder examples
in the main package folder.
Including submodules
- Exporting structs and functions - discourse.julialang
- Need to include one file multiple times, how to avoid warning - discourse.julialang
You need to include
each file exactly once. Including a file
multiple times (once via submodule.jl
and again via main.jl
) creates
completely different types and functions that happen to have the same
names, which is never what you want
You should not need to include the same file multiple times in Julia, so the answer to your question is “don’t do that”. This is an important difference between the way Julia and C++ are used. Instead, you can do:
module A
include("B.jl")
using .B
module C
using ..B
end
end
Where B.jl looks like:
module B
...
end
The using ..B
line lets you load the B module from A inside C without
having to include it twice.
Unit Testing
Base language provides Test
module for simple unit testing
functionality. Simple testing can be done with two macros @test
and
@test_throw
@test [1, 2] + [2, 1] == [3, 3]
@test_throws BoundsError [1, 2, 3][4]
The macro @testset
can be used to group tests into sets. All tests in
a test set will be run, and at the end of the test set a summary will be
printed. If any tests failed or could not be evaluated the test set will
then throw a TestSetException
.
@testset "trigonometric identities" begin
θ = 2/3*π
@test sin(-θ) ≈ -sin(θ)
@test cos(-θ) ≈ cos(θ)
@test sin(2θ) ≈ 2*sin(θ)*cos(θ)
@test cos(2θ) ≈ cos(θ)^2 - sin(θ)^2
end;
Metaprogramming
Expression Objects
Julia has the ability to manipulate Julia code. That’s possible because Julia code itself is expressible as a data type that the language can operate upon, just as it operates on numbers, strings, and arrays. This data type is called Expr. Objects with this data type are referred to as Expr objects or expression objects. Expression objects are different from expressions, which are language forms that return results, such as 3 * 5.
Expression objects often involve Julia Symbols. We can create a Symbol by prepending a colon to a name, as with the attributes, such as :red, that we used when making plots. We can convert a string to a symbol with the Symbol() function as well: Symbol(“red”) == :red.
We can also use colons to construct expression objects by following the colon with an expression in parentheses. To reiterate: 3 * 5 is an expression, while :(3 * 5) is an expression object. If we enter 3 * 5 in the REPL, Julia evaluates the expression and returns 15. If we enter :(3 * 5), or any other expression object, it simply returns what we entered.