Techno Blender
Digitally Yours.

20 Awesome Julia Tips And Tricks For The Advantageous Programmer | by Emmett Boudreau | Jan, 2023

0 113


(image by author)

After using Julia for about 6 years now (wow, it really has been that long,) I have truly fallen in love with this programming language over all other programming languages. In some ways, this comes down to personal preference — multiple dispatch as a paradigm feels like an incredibly natural and intuitive way to program to me personally. However, there are also a host of advantages that utilizing this language for my projects has brought about. With these years of experience and overall enjoyment with the Julia programming language, I have learned an incredible amount. Not only about Julia, but also about multiple dispatch and even programming and programming languages more broadly. With this experience and education has also come some cumulative knowledge of smaller paradigm-shifting tricks that the Julia programming language has to offer. Julia is bringing all of this power to the Scientific domain, Data Science, as well as the broader programming community. Julia could very well end up becoming the Data Science language of the future!

The multiple dispatch paradigm, which Julia has coined into fruition, is an incredible flexible and capable programming paradigm. Not only that, but in my experience it is one of the fastest programming languages to write that I have ever written software in. Julia has a host of advantages when compared with other programming languages that seek to accomplish the same goals; speed, dynamic typing, the list goes on — but none of these advantages are nearly as effective as the paradigm and design of the language itself. With that information out of the way, let’s get into some of my favorite tricks that can be utilized to take advantage of Julia’s design and programming paradigm.

notebook

using Toolips
using TOML
using Statistics

№1: dynamic argument defaults

One thing that I myself often forget you can do in Julia is the ability to provide dynamic arguments to a method. This comes in handy whenever we want an argument that is dependent on the other arguments by default, but also want this argument to be changed. Consider the following example, where we have a web Component from Toolips built inside of a Function that takes two arguments:

function sample_comp(name::String, label::String = "")
comp = h(name, 2, text = label)
comp::Component{:h2}
end

The second argument, label has a default — which is set to nothing. What we could do with this example is make label equal to name in the case that this argument is not provided. This will make it so the label is always something, and can always be changed. After all, how many times have you written a conditional after something like this to check if something is the default in order to do something? Well in Julia we can simply set an arguments default to be a method return from other arguments!

function sample_comp(name::String, label::String = name)
comp = h(name, 2, text = label)
comp::Component{:h2}
end

So now no matter what this Component{:h2} will always have text!

sample_comp("example")[:text]

"example"

It is easy to see where this might come in handy, and this is certainly something to keep in mind whenever we begin writing a new function, as this can make writing said function substantially faster!

№2: type annotations

One very interesting thing you can do in Julia is the annotation of types. Annotating the type of a given variable essentially makes that type become static, which is definitely useful in a variety of situations. Firstly, this can offer a substantial performance benefit so long as your annotations are in the right place. I also found performance with type annotations to often be more consistent. There is more discussion on this, along with some (time macro, very basic) time metrics to support these claims:

An additional benefit here is clarity — annotated types make it incredibly easy to know which types are going where. Additionally, it can be easier to reference things like return types. Personally, I find this to often be a very beneficial thing for clarity, as sometimes you see a variable without knowing its type and then have to reference where it comes from — which could be an entirely separate battle in and of itself. While I would not say I necessarily annotate everything, I do think it is a great idea to utilize annotations in a lot of use-cases to produce a great result. Type annotations for variable names are done in the same way that they are for arguments, using the :: syntax:

annotations_example(x::Int64, y::Int64) = begin
summation::Int64 = x + y
difference::Int64 = x - y
product::Int64 = x * y
sum([product, difference, summation])::Int64
end

I think it is clear to see how this can be advantageous when it comes to clarity, especially if we begin to consider more complex functions. At the very least, we should certainly be taking advantage of return type annotations and argument annotations, for speed, clarity, and multiple dispatch. If you are using a version of Julia that is above 1.8.2, you may also utilize such annotations globally.

№3: where

Another great thing that Julia offers is the where key-word. The where key-word is used to create dispatches for parameters that are provided to the constructor, rather than being a product of the constructor. Typically, parameters leave some clue as to how the object was constructed, however whenever the where syntax is used they also provide information on how the type will be constructed — while still accomplishing that same goal of telling us how the object was constructed. To utilize the where syntax, we will need to build ourselves a constructor with a parameter.

mutable struct MyStructure{T <: Any}
mything::T
number::Bool
end

In Julia, this is what is called an outer constructor. We will now build an inner constructor which will use the where syntax in order to change the both the number field as well as the parameter itself:

mutable struct MyStructure{T <: Any}
mything::T
number::Bool
MyStructure{T}(t::Any) where {T <: Number} = new{T}(t, true)
end

In order to demonstrate the difference, I will do the same with a String , providing false to the number field whenever such a parameter is provided.

mutable struct MyStructure{T <: Any}
mything::T
number::Bool
MyStructure{T}(t::Any) where {T <: Number} = new{T}(t, true)
MyStructure{T}(t::Any) where {T <: AbstractString} = new{T}(t, false)
end

Now let’s take a look at the result, which has allowed us to change the value of different fields based on such a parameter,

mystr = MyStructure{Float64}(5.5)

MyStructure{Float64}(5.5, true)

mystr_str = MyStructure{String}("")

MyStructure{String}("", false)

You may have noticed this is how a lot of structures, such as Vectors in Julia are constructed. This is a very powerful thing because it allows us to change the type on command. Conveniently, we can then write methods for structures with a specific parameter provided using this syntax with multiple dispatch!

№4: macro_str

A feature that I did not know how to use for the longest time is the String macro. With this technique, we can provide a macro before a String in order to automatically do some action with that String as an argument. Consider for example that we wanted to surround convert a String to TOML. We could create a macro that utilizes TOML.parse . Our macro will take one argument, and this will be the String . This is done by providing _str after our macro.

macro toml_str(s::String)
TOML.parse(s)::Dict{String, <:Any}
end

Now we can just provide toml before a String in order to have it automatically parsed into this Dict :

toml"""
x = 5
y = ""
"""

Dict{String, Any} with 2 entries:
"x" => 5
"y" => ""

№5: non-ambiguous typing

As is the case in most programming languages, typing is extremely important. However, in Julia this seems to be exponentially more the case given that the paradigm for the most part revolves around ideas of typing and specific typing to be provided to methods and fields. One thing that can certainly hinder performance of Julian types is what is called ambiguous typing. What this means is that the compiler does not know the type of a given field, so each time it uses that object it has to figure out what type that field is. Consider the following example:

mutable struct Block
building_count::Int64
residents::Int64
burned::Number
end

In this example, the burned field is ambiguous. This is because Number is an abstract type, not an actual type. What this means is that multiple types can be in this field — which is what we want, however the problem is then we have different Blocks constructed with different types for the fields — which has a substantial impact on performance. To fix this, we can either make this a specific type, or we can utilize a parameter in order to make this type change with the constructor that is used. This will change the type of Block to Block{T} . Block{T} is not the same type as Block , thus Julia knows the fields of this type.

mutable struct Block{T <: Number}
building_count::Int64
residents::Int64
burned::T
Block(n::Int64, residents::Int64, burned::Number) = new{typeof(burned)}(n, residents, burned)
end

№6: dispatching parameters

With the new technique utilized in the last tip, we run into a new problem. With the parameter in this constructor, the type Block no longer exists, so how do we dispatch this new type? We can actually provide the sub-type operator in a similar way to how we do in both the parameter of the constructor and the where syntax. Let’s make a function that will work with some different types of our Block :

print_blockt(b::Block{<:Number}) = print(" number")
print_blockt(b::Block{<:Integer}) = print(" integer")
print_blockt(b::Block{<:AbstractFloat}) = print(" float")
print_blockt(b::Block{Bool}) = print(" boolean")

Now let’s call this function with different types:

print_blockt(Block(1, 2, false))

boolean

print_blockt(Block(1, 2, 20))

integer

print_blockt(Block(1, 2, 5.5))

float

print_blockt(Block(1, 2, 5.5 + 2.5im))

print_blockt(Block(1, 2, 5.5 + 2.5im))

number

Alternatively, we could also make Block a sub-type and then provide the abstract type to these methods.

№7: find functions

An incredibly useful set of methods that are integral to building algorithms in Julia are the find functions. From String processing to working with Vectors , and even DataFrames , the find functions in Julia are incredibly useful. In fact, they are so important that I wrote an entire article which goes into more detail on find functions and how to use them in Julia, which can be read here:

For this example, we will look at the following String :

s = """I want to get the position of this and this."""

Given that we need each position inside of this String , we will utilize the findall method. In cases where this is a Vector and not a String , we utilize a Function instead of a String . Here is our String example:

findall("this", s)

2-element Vector{UnitRange{Int64}}:
31:34
40:43

findall will return a Vector , whereas findfirst , findnext , etc. will provide a single object of the same type. In the case of a String , this will be a UnitRange{Int64} . In the case of a Vector , this will be an Int64 , representing the index. For some more complete coverage of this topic, let’s also look at an example of doing the same with a Vector{Int64} :

x = [1, 2, 3, 4, 5, 6]

6-element Vector{Int64}:
1
2
3
4
5
6

findfirst(x -> x > 3, x)

4

Note that the 4 above is the index, not the value. If we wanted to get the value, we would need to index the Vector — this Vector’s indexes are just the same as the element’s value.

№8: multi-line comprehensions

Thanks to Julian syntax, I have nearly given up entirely on for loops. The only exception is when I want to utilize break or continue . This is because comprehensions are not only faster, but more concise — and Julia makes then incredibly easy to read. We can also put just about any feature of a regular Function or for loop into a comprehension; this can be done by utilizing the begin end syntax. For example, let’s say we wanted to turn each one of these into a String with a classification of whether or not the value is above or below the mean:

x = [54, 45, 64, 44, 33, 44, 98]

Let’s start by getting the mean …

mu = mean(x)

And finally, we will write a comprehension that will check whether or not each element is greater or less than this.

pairvec = [begin
if n > mu
"above"
else
"below"
end
end for n in x]

7-element Vector{String}:
"below"
"below"
"above"
"below"
"below"
"below"
"above"

№9: parametric symbolism

Parameters are a great way to denote one type from another whenever types are provided, but is there any other way that we can denote type without carrying types as a parameter? Parameters can be a few different things, including Integers , Unions , Symbols, and of course the essential Type . Being able to denote type with a Symbol , in particular, is what I want to focus on because this often comes in handy. A great example of where this concept is applied inside of Julia’s Base module is the MIME type. Using this technique, we can denote type by data — in this case a String converted into a Symbol .

mutable struct Fruit{T <: Any}
seeds::Int64
Fruit(name::String, seeds::Int64) = new{Symbol(name)}(seeds)
end

With this simple structure, we can create Fruit{:pepper} and Fruit{:apple} — yes, peppers are fruits.

apple = Fruit("apple", 5)

Fruit{:apple}(5)

pepper = Fruit("pepper", 40)

Fruit{:pepper}(40)

Now we can create individual dispatches for each of these:

taste(f::Fruit{:apple}) = println("it's sweet, sugary, and delicious.")
taste(f::Fruit{:pepper}) = println("It is savory .... but HOT!")

Each fruit will then be dispatched dependent on this parameter.

taste(apple)

it's sweet, sugary, and delicious.

taste(pepper)

It is savory .... but HOT!

№10: unions

Another type that can go into the parameters of another type is the Union . Unions are a bit different to parameters themselves, though not substantially different — the only difference is that a Union can only contain Types. Learning to work with the Union well is certainly going to help one to master the nuances of working with parameters in Julia. However, providing a Union as a parameter will allow you to place multiple parameters into a single one — which can certainly apply in some scenarios. Let’s add to our Fruit constructor from before a new inner constructor to demonstrate this concept:

mutable struct Fruit{T <: Any}
seeds::Int64
Fruit(name::String, seeds::Int64) = new{Symbol(name)}(seeds)
Fruit{T}(seeds::Int64) where {T <: Any} = new{T}(seeds)
end

Now let’s provide a Union :

Fruit{Union{Int64, String}}(55)

Fruit{Union{Int64, String}}(55)

This is for more complex structures with more articulate typing, such as some type of Vector that contains Integers and Floats . This might not come in handy that often, but when it does you will be thankful that you know about it!

№11: methods method

I have talked before about how I think introspection is a very powerful feature. In Julia, you can introspect everything from modules, to types, and yes — even functions. Of course, functions in Julia have multiple methods — given the multiple dispatch — so we cannot introspect a function as much as we can introspect the various methods that are defined under the type of that function. We can retrieve all of the methods of a method with the methods method — and I understand that sounds confusing because I just said method so many times, but this really is relatively simple.

Calling the methods method on a Function will return a MethodList .

m = methods(taste)

# 2 methods for generic function taste:
taste(f::Fruit{:apple}) in Main at In[64]:1
taste(f::Fruit{:pepper}) in Main at In[64]:2

№12: method signatures

With the methods method discussed in the previous tip, we also gain access to method signatures. This data is stored inside of the sig field of a method, and gives the types of the arguments and the actual type of the Function itself.

m[1].sig

Tuple{typeof(taste), Fruit{:apple}}

We can then index the signature to get more details on the method. Note that the information is contained within parameters, so we will need to call the parameters field on sig :

m[1].sig.parameters[2]

Fruit{:apple}

Now let’s use this to iteratively call each taste method:

for fruit_method in m
T = fruit_method.sig.parameters[2]
taste(T(1))
end

it's sweet, sugary, and delicious.
It is savory .... but HOT!

Now if we define a new method for taste

taste(f::Fruit{:orange}) = println("it's super sweet, with a little bit of sour")

… we can run this again and get a different result.

m = methods(taste)
for fruit_method in m
T = fruit_method.sig.parameters[2]
taste(T(1))
end

it's sweet, sugary, and delicious.
It is savory .... but HOT!
it's super sweet, with a little bit of sour

№13: extending vect

A lesser-known function that can be extended to change the syntax of Julia quite dramatically is vect . This is the function that is called every time we utilize [] . Needless to say, there is some cool syntax that we can appropriate using vect — an example that comes to mind would be something like putting database queries into here. Let’s build a small example of this using a global Dict :

db = Dict{String, AbstractVector}("b" => [], "b" => [])

Dict{String, AbstractVector} with 1 entry:
"b" => Any[]

Let’s create a new QueryWord type, which will utilize the symbolic dispatch we discussed earlier to create different types.

mutable struct QueryWord{T<:Any} QueryWord{T}() where {T<:Any} = new{T}() end

Finally, we will finish this by extending vect . Of course, we need to explicitly import it first!

import Base: vect
function vect(qw::QueryWord{:select}, args ...)

end

Inside of this function, I will check for what args contains. I am not going to use throws or anything to make this function really usable by anyone else — so note that there are ways this example could be called that have errors without knowable output. Fortunately, this is not going anywhere but this notebook, so…

function vect(qw::QueryWord{:select}, args ...)
if typeof(args[1]) == typeof(*)
db[args[2]]::AbstractVector
elseif typeof(args[1]) == Symbol
db[args[1]][args[2]]
end
end

Finally, I am going to make another String macro for our Query .

macro query_str(s::String)
QueryWord{Symbol(s)}()
end

And the result!

[query"select", *, "b"]

Any[]

Now that is some cool syntax!

№14: broadcasting

It is likely many of us know about broadcasting in Julia. Broadcasting allows us to use a function across all elements in a given collection with one simple function call. While it is pretty standard that broadcasting is used across the operators in Julia, such is the case for operations like .* and .+ , you might not have realized that you can actually broadcast any method onto any type, so long as the method is properly callable onto that collection. The trick is to simply use the @. macro, for example:

fruits = [apple, pepper]
@. taste(fruits)

it's sweet, sugary, and delicious.
It is savory .... but HOT!
2-element Vector{Nothing}:
nothing
nothing

If you would like to learn more about broadcasting in Julia, here is an article I wrote on the topic which goes into more detail:

№15: multiplication syntax

With all of the wonderful applications of symbolic parameters we have seen thus far, it might be reasonable to assume the concept has run out of steam. This assumption, however, would be entirely wrong — as there is even more awesome syntax this is capable of producing, and one really cool syntax is the multiplication syntax. Because Julia is built to look like math papers, one thing that is supported is the product of variables and a number when the two are next to one another. For example,

x = 5
5x

25

We can use this to our advantage to produce some pretty awesome syntax. I will be reusing the QueryWord type for this, but we will use this to create a new form of measurement — the millimeter. In order to change what this syntax does with our type, we simply need to provide a new method for * , which of course means another explicit import.

import Base: *
*(i::Int64, qw::QueryWord{:mm}) = "$(i)mm"

Now we will simply define a constant mm which is equal to this QueryWord .

const mm = QueryWord{:mm}()

Now we can provide the mm constant after an Int64 :

55mm

"55mm"

№16: anonymous … objects?

It is likely that most Julia programmers are aware of anonymous functions. These are functions that can be defined without a name, and are typically used as arguments. This is a better choice than closures or global methods for both memory and convenience in many cases. If you have not heard of them before, you may read up on what exactly they are here:

While anonymous functions are relatively universally known to most Julia users, the full extent that the logical right operator can be used to create anonymous things might not be known. We can actually store data anonymously without type using this technique. To do so, we simply provide each field in a tuple to an empty argument list on the left hand side of the operator.

function example(s::String)
function show()
print(s)
end
() -> (s;show)
end

This creates a simple, yet effective result.

myexamp = example("home")

#7 (generic function with 1 method)

myexamp.s

"home"

myexamp.show()

home

№17: “ object-oriented programming”

While Julia is not an object-oriented programming language, we can actually get really close by utilizing inner constructors to create functions. Such functions can then be used as fields, and are callable with their dispatches! This might not necessarily be “ object-oriented” per say, but it mimics the traditional syntax of an object-oriented programming language from a high-level, which seems to be in-line with the theme of this entire article.

mutable struct MyObject
name::String
myfunc::Function
function MyObject(name::String)
myfunc()::Nothing = begin
[println(c) for c in name]
nothing
end
new(name, myfunc)
end
end

Now we can call myfunc exactly how we would expect in a traditional object-oriented programming language!

MyObject("steve").myfunc()

s
t
e
v
e

№18: constant fields (Julia 1.8.2 +)

A relatively new feature to the Julia programming language is the ability to make certain fields constant while other fields remain mutable. This was added in Julia 1.8, and is a pretty awesome feature — certainly not something I have seen in any other language. I could also see a variety of applications for something like this. The implementation is also incredibly easy and simple to utilize, simply provide the const keyword before a field name inside of an outer constructor:

mutable struct ConstantExample
const x::Int64
y::Int64
end

№19: top-level constructor

One thing that I like to do when creating my constructors is create a top-level constructor. What does this mean? I like to create a simplified constructor with the typical expected inputs which can take any parameters as arguments. Why do I do this, the answer is simple — do not repeat yourself. Instead of building the same fields in six different constructors, you can much more easily build a single constructor and then call that constructor than make a bunch of different constructors that mostly do the same thing.

mutable struct LastConstructorThankGoodness
name::String
count::Int64
place::String
name_length::Int64
place_length::Int64
function LastConstructorThankGoodness(name::String, count::Int64, place::String)

end
end

The example above is a rather simple example, but it is easy to see how these types of fields could potentially get a lot more complicated when working with real-world constructors and applications. We have two things that need to be calculated with our arguments, name_length and place_length . These are just going to be the length of each String .

mutable struct LastConstructorThankGoodness
name::String
count::Int64
place::String
name_length::Int64
place_length::Int64
function LastConstructorThankGoodness(name::String, count::Int64, place::String)
new(name, count, place, length(name), length(place))
end
end

That is nice; but what if I want to add another constructor?

    function LastConstructorThankGoodness(; args ...)
name::String = ""
count::Int64 = 0
place::String = ""
[begin
if arg[1] == :name
name = arg[2]
elseif arg[1] == :count
count = arg[2]
elseif arg[1] == :place
place = arg[2]
end
end for arg in args]
end

In this new inner constructor, we could of course recalculate the length of our strings. In this case, this is not too complicated if we do so. However, especially when our types are more complicated, it might be smart to call the other constructor instead of calling new :

mutable struct LastConstructorThankGoodness
name::String
count::Int64
place::String
name_length::Int64
place_length::Int64
function LastConstructorThankGoodness(name::String, count::Int64, place::String)
new(name, count, place, length(name), length(place))
end
function LastConstructorThankGoodness(; args ...)
name::String = ""
count::Int64 = 0
place::String = ""
[begin
if arg[1] == :name
name = arg[2]
elseif arg[1] == :count
count = arg[2]
elseif arg[1] == :place
place = arg[2]
end
end for arg in args]
LastConstructorThankGoodness(name, count, place)
end
end
LastConstructorThankGoodness(name = "hi", place = "home", count = 5)

LastConstructorThankGoodness("hi", 5, "home", 2, 4)

№20: you can in fact extend :

Extensibility is an amazing feature of the Julia language, and probably one of my favorite things that sets Julia apart from other languages. There are not many other languages that parallel the extensible nature of methods and modules in Julia. A great example, which people often point to and we have already done in this article, is extending operators and symbols. There are a few operators this does not work with, however. Prominent examples include logical operators, bool operators, and the sub-type operator. One symbol that I always thought was in a similar boat in Julia was the : method. This, I found out, is actually incorrect! The method is simply imported and dispatched as (:) !

import Base: (:)
(:)(s1::String, s2::String) = length(s1):length(s2)

"hi":"hello"
2:5

We can also do this consecutively, which is pretty cool, as well!

(:)(s1::String, s2::String, s::String) = "$s2 is between $s1 and $s"

"one":"two":"three"

"two is between one and three"


(image by author)

After using Julia for about 6 years now (wow, it really has been that long,) I have truly fallen in love with this programming language over all other programming languages. In some ways, this comes down to personal preference — multiple dispatch as a paradigm feels like an incredibly natural and intuitive way to program to me personally. However, there are also a host of advantages that utilizing this language for my projects has brought about. With these years of experience and overall enjoyment with the Julia programming language, I have learned an incredible amount. Not only about Julia, but also about multiple dispatch and even programming and programming languages more broadly. With this experience and education has also come some cumulative knowledge of smaller paradigm-shifting tricks that the Julia programming language has to offer. Julia is bringing all of this power to the Scientific domain, Data Science, as well as the broader programming community. Julia could very well end up becoming the Data Science language of the future!

The multiple dispatch paradigm, which Julia has coined into fruition, is an incredible flexible and capable programming paradigm. Not only that, but in my experience it is one of the fastest programming languages to write that I have ever written software in. Julia has a host of advantages when compared with other programming languages that seek to accomplish the same goals; speed, dynamic typing, the list goes on — but none of these advantages are nearly as effective as the paradigm and design of the language itself. With that information out of the way, let’s get into some of my favorite tricks that can be utilized to take advantage of Julia’s design and programming paradigm.

notebook

using Toolips
using TOML
using Statistics

№1: dynamic argument defaults

One thing that I myself often forget you can do in Julia is the ability to provide dynamic arguments to a method. This comes in handy whenever we want an argument that is dependent on the other arguments by default, but also want this argument to be changed. Consider the following example, where we have a web Component from Toolips built inside of a Function that takes two arguments:

function sample_comp(name::String, label::String = "")
comp = h(name, 2, text = label)
comp::Component{:h2}
end

The second argument, label has a default — which is set to nothing. What we could do with this example is make label equal to name in the case that this argument is not provided. This will make it so the label is always something, and can always be changed. After all, how many times have you written a conditional after something like this to check if something is the default in order to do something? Well in Julia we can simply set an arguments default to be a method return from other arguments!

function sample_comp(name::String, label::String = name)
comp = h(name, 2, text = label)
comp::Component{:h2}
end

So now no matter what this Component{:h2} will always have text!

sample_comp("example")[:text]

"example"

It is easy to see where this might come in handy, and this is certainly something to keep in mind whenever we begin writing a new function, as this can make writing said function substantially faster!

№2: type annotations

One very interesting thing you can do in Julia is the annotation of types. Annotating the type of a given variable essentially makes that type become static, which is definitely useful in a variety of situations. Firstly, this can offer a substantial performance benefit so long as your annotations are in the right place. I also found performance with type annotations to often be more consistent. There is more discussion on this, along with some (time macro, very basic) time metrics to support these claims:

An additional benefit here is clarity — annotated types make it incredibly easy to know which types are going where. Additionally, it can be easier to reference things like return types. Personally, I find this to often be a very beneficial thing for clarity, as sometimes you see a variable without knowing its type and then have to reference where it comes from — which could be an entirely separate battle in and of itself. While I would not say I necessarily annotate everything, I do think it is a great idea to utilize annotations in a lot of use-cases to produce a great result. Type annotations for variable names are done in the same way that they are for arguments, using the :: syntax:

annotations_example(x::Int64, y::Int64) = begin
summation::Int64 = x + y
difference::Int64 = x - y
product::Int64 = x * y
sum([product, difference, summation])::Int64
end

I think it is clear to see how this can be advantageous when it comes to clarity, especially if we begin to consider more complex functions. At the very least, we should certainly be taking advantage of return type annotations and argument annotations, for speed, clarity, and multiple dispatch. If you are using a version of Julia that is above 1.8.2, you may also utilize such annotations globally.

№3: where

Another great thing that Julia offers is the where key-word. The where key-word is used to create dispatches for parameters that are provided to the constructor, rather than being a product of the constructor. Typically, parameters leave some clue as to how the object was constructed, however whenever the where syntax is used they also provide information on how the type will be constructed — while still accomplishing that same goal of telling us how the object was constructed. To utilize the where syntax, we will need to build ourselves a constructor with a parameter.

mutable struct MyStructure{T <: Any}
mything::T
number::Bool
end

In Julia, this is what is called an outer constructor. We will now build an inner constructor which will use the where syntax in order to change the both the number field as well as the parameter itself:

mutable struct MyStructure{T <: Any}
mything::T
number::Bool
MyStructure{T}(t::Any) where {T <: Number} = new{T}(t, true)
end

In order to demonstrate the difference, I will do the same with a String , providing false to the number field whenever such a parameter is provided.

mutable struct MyStructure{T <: Any}
mything::T
number::Bool
MyStructure{T}(t::Any) where {T <: Number} = new{T}(t, true)
MyStructure{T}(t::Any) where {T <: AbstractString} = new{T}(t, false)
end

Now let’s take a look at the result, which has allowed us to change the value of different fields based on such a parameter,

mystr = MyStructure{Float64}(5.5)

MyStructure{Float64}(5.5, true)

mystr_str = MyStructure{String}("")

MyStructure{String}("", false)

You may have noticed this is how a lot of structures, such as Vectors in Julia are constructed. This is a very powerful thing because it allows us to change the type on command. Conveniently, we can then write methods for structures with a specific parameter provided using this syntax with multiple dispatch!

№4: macro_str

A feature that I did not know how to use for the longest time is the String macro. With this technique, we can provide a macro before a String in order to automatically do some action with that String as an argument. Consider for example that we wanted to surround convert a String to TOML. We could create a macro that utilizes TOML.parse . Our macro will take one argument, and this will be the String . This is done by providing _str after our macro.

macro toml_str(s::String)
TOML.parse(s)::Dict{String, <:Any}
end

Now we can just provide toml before a String in order to have it automatically parsed into this Dict :

toml"""
x = 5
y = ""
"""

Dict{String, Any} with 2 entries:
"x" => 5
"y" => ""

№5: non-ambiguous typing

As is the case in most programming languages, typing is extremely important. However, in Julia this seems to be exponentially more the case given that the paradigm for the most part revolves around ideas of typing and specific typing to be provided to methods and fields. One thing that can certainly hinder performance of Julian types is what is called ambiguous typing. What this means is that the compiler does not know the type of a given field, so each time it uses that object it has to figure out what type that field is. Consider the following example:

mutable struct Block
building_count::Int64
residents::Int64
burned::Number
end

In this example, the burned field is ambiguous. This is because Number is an abstract type, not an actual type. What this means is that multiple types can be in this field — which is what we want, however the problem is then we have different Blocks constructed with different types for the fields — which has a substantial impact on performance. To fix this, we can either make this a specific type, or we can utilize a parameter in order to make this type change with the constructor that is used. This will change the type of Block to Block{T} . Block{T} is not the same type as Block , thus Julia knows the fields of this type.

mutable struct Block{T <: Number}
building_count::Int64
residents::Int64
burned::T
Block(n::Int64, residents::Int64, burned::Number) = new{typeof(burned)}(n, residents, burned)
end

№6: dispatching parameters

With the new technique utilized in the last tip, we run into a new problem. With the parameter in this constructor, the type Block no longer exists, so how do we dispatch this new type? We can actually provide the sub-type operator in a similar way to how we do in both the parameter of the constructor and the where syntax. Let’s make a function that will work with some different types of our Block :

print_blockt(b::Block{<:Number}) = print(" number")
print_blockt(b::Block{<:Integer}) = print(" integer")
print_blockt(b::Block{<:AbstractFloat}) = print(" float")
print_blockt(b::Block{Bool}) = print(" boolean")

Now let’s call this function with different types:

print_blockt(Block(1, 2, false))

boolean

print_blockt(Block(1, 2, 20))

integer

print_blockt(Block(1, 2, 5.5))

float

print_blockt(Block(1, 2, 5.5 + 2.5im))

print_blockt(Block(1, 2, 5.5 + 2.5im))

number

Alternatively, we could also make Block a sub-type and then provide the abstract type to these methods.

№7: find functions

An incredibly useful set of methods that are integral to building algorithms in Julia are the find functions. From String processing to working with Vectors , and even DataFrames , the find functions in Julia are incredibly useful. In fact, they are so important that I wrote an entire article which goes into more detail on find functions and how to use them in Julia, which can be read here:

For this example, we will look at the following String :

s = """I want to get the position of this and this."""

Given that we need each position inside of this String , we will utilize the findall method. In cases where this is a Vector and not a String , we utilize a Function instead of a String . Here is our String example:

findall("this", s)

2-element Vector{UnitRange{Int64}}:
31:34
40:43

findall will return a Vector , whereas findfirst , findnext , etc. will provide a single object of the same type. In the case of a String , this will be a UnitRange{Int64} . In the case of a Vector , this will be an Int64 , representing the index. For some more complete coverage of this topic, let’s also look at an example of doing the same with a Vector{Int64} :

x = [1, 2, 3, 4, 5, 6]

6-element Vector{Int64}:
1
2
3
4
5
6

findfirst(x -> x > 3, x)

4

Note that the 4 above is the index, not the value. If we wanted to get the value, we would need to index the Vector — this Vector’s indexes are just the same as the element’s value.

№8: multi-line comprehensions

Thanks to Julian syntax, I have nearly given up entirely on for loops. The only exception is when I want to utilize break or continue . This is because comprehensions are not only faster, but more concise — and Julia makes then incredibly easy to read. We can also put just about any feature of a regular Function or for loop into a comprehension; this can be done by utilizing the begin end syntax. For example, let’s say we wanted to turn each one of these into a String with a classification of whether or not the value is above or below the mean:

x = [54, 45, 64, 44, 33, 44, 98]

Let’s start by getting the mean …

mu = mean(x)

And finally, we will write a comprehension that will check whether or not each element is greater or less than this.

pairvec = [begin
if n > mu
"above"
else
"below"
end
end for n in x]

7-element Vector{String}:
"below"
"below"
"above"
"below"
"below"
"below"
"above"

№9: parametric symbolism

Parameters are a great way to denote one type from another whenever types are provided, but is there any other way that we can denote type without carrying types as a parameter? Parameters can be a few different things, including Integers , Unions , Symbols, and of course the essential Type . Being able to denote type with a Symbol , in particular, is what I want to focus on because this often comes in handy. A great example of where this concept is applied inside of Julia’s Base module is the MIME type. Using this technique, we can denote type by data — in this case a String converted into a Symbol .

mutable struct Fruit{T <: Any}
seeds::Int64
Fruit(name::String, seeds::Int64) = new{Symbol(name)}(seeds)
end

With this simple structure, we can create Fruit{:pepper} and Fruit{:apple} — yes, peppers are fruits.

apple = Fruit("apple", 5)

Fruit{:apple}(5)

pepper = Fruit("pepper", 40)

Fruit{:pepper}(40)

Now we can create individual dispatches for each of these:

taste(f::Fruit{:apple}) = println("it's sweet, sugary, and delicious.")
taste(f::Fruit{:pepper}) = println("It is savory .... but HOT!")

Each fruit will then be dispatched dependent on this parameter.

taste(apple)

it's sweet, sugary, and delicious.

taste(pepper)

It is savory .... but HOT!

№10: unions

Another type that can go into the parameters of another type is the Union . Unions are a bit different to parameters themselves, though not substantially different — the only difference is that a Union can only contain Types. Learning to work with the Union well is certainly going to help one to master the nuances of working with parameters in Julia. However, providing a Union as a parameter will allow you to place multiple parameters into a single one — which can certainly apply in some scenarios. Let’s add to our Fruit constructor from before a new inner constructor to demonstrate this concept:

mutable struct Fruit{T <: Any}
seeds::Int64
Fruit(name::String, seeds::Int64) = new{Symbol(name)}(seeds)
Fruit{T}(seeds::Int64) where {T <: Any} = new{T}(seeds)
end

Now let’s provide a Union :

Fruit{Union{Int64, String}}(55)

Fruit{Union{Int64, String}}(55)

This is for more complex structures with more articulate typing, such as some type of Vector that contains Integers and Floats . This might not come in handy that often, but when it does you will be thankful that you know about it!

№11: methods method

I have talked before about how I think introspection is a very powerful feature. In Julia, you can introspect everything from modules, to types, and yes — even functions. Of course, functions in Julia have multiple methods — given the multiple dispatch — so we cannot introspect a function as much as we can introspect the various methods that are defined under the type of that function. We can retrieve all of the methods of a method with the methods method — and I understand that sounds confusing because I just said method so many times, but this really is relatively simple.

Calling the methods method on a Function will return a MethodList .

m = methods(taste)

# 2 methods for generic function taste:
taste(f::Fruit{:apple}) in Main at In[64]:1
taste(f::Fruit{:pepper}) in Main at In[64]:2

№12: method signatures

With the methods method discussed in the previous tip, we also gain access to method signatures. This data is stored inside of the sig field of a method, and gives the types of the arguments and the actual type of the Function itself.

m[1].sig

Tuple{typeof(taste), Fruit{:apple}}

We can then index the signature to get more details on the method. Note that the information is contained within parameters, so we will need to call the parameters field on sig :

m[1].sig.parameters[2]

Fruit{:apple}

Now let’s use this to iteratively call each taste method:

for fruit_method in m
T = fruit_method.sig.parameters[2]
taste(T(1))
end

it's sweet, sugary, and delicious.
It is savory .... but HOT!

Now if we define a new method for taste

taste(f::Fruit{:orange}) = println("it's super sweet, with a little bit of sour")

… we can run this again and get a different result.

m = methods(taste)
for fruit_method in m
T = fruit_method.sig.parameters[2]
taste(T(1))
end

it's sweet, sugary, and delicious.
It is savory .... but HOT!
it's super sweet, with a little bit of sour

№13: extending vect

A lesser-known function that can be extended to change the syntax of Julia quite dramatically is vect . This is the function that is called every time we utilize [] . Needless to say, there is some cool syntax that we can appropriate using vect — an example that comes to mind would be something like putting database queries into here. Let’s build a small example of this using a global Dict :

db = Dict{String, AbstractVector}("b" => [], "b" => [])

Dict{String, AbstractVector} with 1 entry:
"b" => Any[]

Let’s create a new QueryWord type, which will utilize the symbolic dispatch we discussed earlier to create different types.

mutable struct QueryWord{T<:Any} QueryWord{T}() where {T<:Any} = new{T}() end

Finally, we will finish this by extending vect . Of course, we need to explicitly import it first!

import Base: vect
function vect(qw::QueryWord{:select}, args ...)

end

Inside of this function, I will check for what args contains. I am not going to use throws or anything to make this function really usable by anyone else — so note that there are ways this example could be called that have errors without knowable output. Fortunately, this is not going anywhere but this notebook, so…

function vect(qw::QueryWord{:select}, args ...)
if typeof(args[1]) == typeof(*)
db[args[2]]::AbstractVector
elseif typeof(args[1]) == Symbol
db[args[1]][args[2]]
end
end

Finally, I am going to make another String macro for our Query .

macro query_str(s::String)
QueryWord{Symbol(s)}()
end

And the result!

[query"select", *, "b"]

Any[]

Now that is some cool syntax!

№14: broadcasting

It is likely many of us know about broadcasting in Julia. Broadcasting allows us to use a function across all elements in a given collection with one simple function call. While it is pretty standard that broadcasting is used across the operators in Julia, such is the case for operations like .* and .+ , you might not have realized that you can actually broadcast any method onto any type, so long as the method is properly callable onto that collection. The trick is to simply use the @. macro, for example:

fruits = [apple, pepper]
@. taste(fruits)

it's sweet, sugary, and delicious.
It is savory .... but HOT!
2-element Vector{Nothing}:
nothing
nothing

If you would like to learn more about broadcasting in Julia, here is an article I wrote on the topic which goes into more detail:

№15: multiplication syntax

With all of the wonderful applications of symbolic parameters we have seen thus far, it might be reasonable to assume the concept has run out of steam. This assumption, however, would be entirely wrong — as there is even more awesome syntax this is capable of producing, and one really cool syntax is the multiplication syntax. Because Julia is built to look like math papers, one thing that is supported is the product of variables and a number when the two are next to one another. For example,

x = 5
5x

25

We can use this to our advantage to produce some pretty awesome syntax. I will be reusing the QueryWord type for this, but we will use this to create a new form of measurement — the millimeter. In order to change what this syntax does with our type, we simply need to provide a new method for * , which of course means another explicit import.

import Base: *
*(i::Int64, qw::QueryWord{:mm}) = "$(i)mm"

Now we will simply define a constant mm which is equal to this QueryWord .

const mm = QueryWord{:mm}()

Now we can provide the mm constant after an Int64 :

55mm

"55mm"

№16: anonymous … objects?

It is likely that most Julia programmers are aware of anonymous functions. These are functions that can be defined without a name, and are typically used as arguments. This is a better choice than closures or global methods for both memory and convenience in many cases. If you have not heard of them before, you may read up on what exactly they are here:

While anonymous functions are relatively universally known to most Julia users, the full extent that the logical right operator can be used to create anonymous things might not be known. We can actually store data anonymously without type using this technique. To do so, we simply provide each field in a tuple to an empty argument list on the left hand side of the operator.

function example(s::String)
function show()
print(s)
end
() -> (s;show)
end

This creates a simple, yet effective result.

myexamp = example("home")

#7 (generic function with 1 method)

myexamp.s

"home"

myexamp.show()

home

№17: “ object-oriented programming”

While Julia is not an object-oriented programming language, we can actually get really close by utilizing inner constructors to create functions. Such functions can then be used as fields, and are callable with their dispatches! This might not necessarily be “ object-oriented” per say, but it mimics the traditional syntax of an object-oriented programming language from a high-level, which seems to be in-line with the theme of this entire article.

mutable struct MyObject
name::String
myfunc::Function
function MyObject(name::String)
myfunc()::Nothing = begin
[println(c) for c in name]
nothing
end
new(name, myfunc)
end
end

Now we can call myfunc exactly how we would expect in a traditional object-oriented programming language!

MyObject("steve").myfunc()

s
t
e
v
e

№18: constant fields (Julia 1.8.2 +)

A relatively new feature to the Julia programming language is the ability to make certain fields constant while other fields remain mutable. This was added in Julia 1.8, and is a pretty awesome feature — certainly not something I have seen in any other language. I could also see a variety of applications for something like this. The implementation is also incredibly easy and simple to utilize, simply provide the const keyword before a field name inside of an outer constructor:

mutable struct ConstantExample
const x::Int64
y::Int64
end

№19: top-level constructor

One thing that I like to do when creating my constructors is create a top-level constructor. What does this mean? I like to create a simplified constructor with the typical expected inputs which can take any parameters as arguments. Why do I do this, the answer is simple — do not repeat yourself. Instead of building the same fields in six different constructors, you can much more easily build a single constructor and then call that constructor than make a bunch of different constructors that mostly do the same thing.

mutable struct LastConstructorThankGoodness
name::String
count::Int64
place::String
name_length::Int64
place_length::Int64
function LastConstructorThankGoodness(name::String, count::Int64, place::String)

end
end

The example above is a rather simple example, but it is easy to see how these types of fields could potentially get a lot more complicated when working with real-world constructors and applications. We have two things that need to be calculated with our arguments, name_length and place_length . These are just going to be the length of each String .

mutable struct LastConstructorThankGoodness
name::String
count::Int64
place::String
name_length::Int64
place_length::Int64
function LastConstructorThankGoodness(name::String, count::Int64, place::String)
new(name, count, place, length(name), length(place))
end
end

That is nice; but what if I want to add another constructor?

    function LastConstructorThankGoodness(; args ...)
name::String = ""
count::Int64 = 0
place::String = ""
[begin
if arg[1] == :name
name = arg[2]
elseif arg[1] == :count
count = arg[2]
elseif arg[1] == :place
place = arg[2]
end
end for arg in args]
end

In this new inner constructor, we could of course recalculate the length of our strings. In this case, this is not too complicated if we do so. However, especially when our types are more complicated, it might be smart to call the other constructor instead of calling new :

mutable struct LastConstructorThankGoodness
name::String
count::Int64
place::String
name_length::Int64
place_length::Int64
function LastConstructorThankGoodness(name::String, count::Int64, place::String)
new(name, count, place, length(name), length(place))
end
function LastConstructorThankGoodness(; args ...)
name::String = ""
count::Int64 = 0
place::String = ""
[begin
if arg[1] == :name
name = arg[2]
elseif arg[1] == :count
count = arg[2]
elseif arg[1] == :place
place = arg[2]
end
end for arg in args]
LastConstructorThankGoodness(name, count, place)
end
end
LastConstructorThankGoodness(name = "hi", place = "home", count = 5)

LastConstructorThankGoodness("hi", 5, "home", 2, 4)

№20: you can in fact extend :

Extensibility is an amazing feature of the Julia language, and probably one of my favorite things that sets Julia apart from other languages. There are not many other languages that parallel the extensible nature of methods and modules in Julia. A great example, which people often point to and we have already done in this article, is extending operators and symbols. There are a few operators this does not work with, however. Prominent examples include logical operators, bool operators, and the sub-type operator. One symbol that I always thought was in a similar boat in Julia was the : method. This, I found out, is actually incorrect! The method is simply imported and dispatched as (:) !

import Base: (:)
(:)(s1::String, s2::String) = length(s1):length(s2)

"hi":"hello"
2:5

We can also do this consecutively, which is pretty cool, as well!

(:)(s1::String, s2::String, s::String) = "$s2 is between $s1 and $s"

"one":"two":"three"

"two is between one and three"

FOLLOW US ON GOOGLE NEWS

Read original article here

Denial of responsibility! Techno Blender is an automatic aggregator of the all world’s media. In each content, the hyperlink to the primary source is specified. All trademarks belong to their rightful owners, all materials to their authors. If you are the owner of the content and do not want us to publish your materials, please contact us by email – [email protected]. The content will be deleted within 24 hours.
Leave a comment