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 userauthors 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

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.


No notes link to this note