5 Julia Tricks For High-Level Syntax
Some simple Julia tricks to make more accessible syntax for your module.

introduction
Although the popular understanding of Julia is simply that of a fast and easy to use programming language, the real case for the language goes a lot deeper than this surface level analysis and the language is patently distinct from any other programming language that has ever existed. The center-piece to this novelty is multiple dispatch, the ability to write/create based on functions and argument types, as a programming paradigm. While a language revolving around multiple dispatch is not entirely novel, (see my piece on Standard Meta Language) Julia brings novel concepts to programming through the paradigm of multiple dispatch, with some rather convincing results.
One important aspect of any programming language to pay attention to, especially for new programmers or new languages, is programming syntax. Julia software — or any software, really — written without the proper syntax will not compile. In many cases, syntax might be a significant barrier to education when it comes to programming; writing in syntax is a lot like writing a second language, it is just something you must get used to. As a result, high-level programming languages like Julia have made efforts to make syntax far easier to interpret for the average user. Though Julia’s syntax interpretation alone is pretty forgiving in many regards, there are also a number of awesome features the language comes with that help facilitate an extremely high-level syntax that at times effectively looks identical to a paper, formula, or document with the right syntax. In the typical line of Julian thinking, this high-level system is forged through the programming paradigm of multiple dispatch. Using multiple dispatch, there are a myriad of different techniques we can use to create useful and declarative high-level syntax in Julia.
source
constants
The first of these techniques I would like to discuss is using constants to simplify common expressions. There are two things we should know about Julia’s behavior when it comes to assertion in order to use constants for this purpose:
- Anything placed next to a sub-type
Numberwill be multiplied by that number. For example
julia> x = 5
5
julia> 5x
25This calls the multiplication operator, * . Constants can also be used to alias a type under a different name. For example, a Vector{Int64} in Julia is a type alias for a 1-dimensional Array , or an Array{Int64, 1}.
julia> Vector{Int64} == Array{Int64, 1}
trueWhen declaring a Function under a Typename (a constructor) or using assertion we have the ability to use the where syntax, and this syntax also carries over to constants and assertion with constants. This being said, we can use a constant to represent a type and its parameters.
julia> mutable struct Name
value::String
end
julia> const Names = Vector{Name}
Vector{Name} (alias for Array{Name, 1})
With the example above, we have aliased Array{Name, 1} as Names . This being considered, it is now possible to dispatch using the simple naming scheme of Name !
myvec = [Name("emmy"), Name("hank")]
function shownames(myvec::Names)
[println(n.value) for n in myvec]; nothing
end
julia> shownames(myvec)
emmy
hankTo add a type parameter, we simply add the parameter to the const declaration, on the other side we can provide where to sub-type or type the information.
julia> abstract type Identifier end
julia> abstract type NameIdentifier <: Identifier end
julia> mutable struct Name <: NameIdentifier
value::String
end
julia> mutable struct UserInfo{N <: Identifier}
n::N
url::String
end
julia> const NamedUser{T} = UserInfo{T} where T <: NameIdentifier
NamedUser (alias for UserInfo{T} where T<:NameIdentifier)
julia> NamedUser{Name}(Name("emma"), "https://github.com/emmaccode")
NamedUser{Name}(Name("emma"), "https://github.com/emmaccode")This is not all that constants have to offer, either! Thanks to the * operator being used for numbers next to variable names, multiple dispatch can be used to extend constants with a type signature. This can be incredibly useful for units and measurements, and my personal usage for this came in the form of measurements for my templating syntax.
mutable struct WebMeasure{format} end
import Base: *
*(i::Any, p::WebMeasure{<:Any}) = "$(i)$(typeof(p).parameters[1])"
const pt = WebMeasure{:pt}()
WebMeasure{:pt}()
const mm = WebMeasure{:mm}()
WebMeasure{:mm}()Other algorithms may be more complicated of course, but this worked for my implementation, and using this type of syntax a lot of different things are composable.
x = 55mm
"55mm"
y = 25pt
"25pt"macros
Macros are another vital tool that bring a lot of alternate functionality and syntax to the Julia language. Macros allow us to do two main things in Julia:
- Get a Julia type provided to the macro when it is created.
- Get Julia code provided to the macro and the scope it was evaluated in as an
Expr.
In the latter case, we use the standard syntax for creating a macro .
julia> macro example(e::Expr)
println(e.head); println(e.args)
end
@example (macro with 3 methods)
julia> @example x += 5
+=
Any[:x, 5]From here, we are able to parse the fields of our Expr and make some type of evaluation, ideally in the scope it came from — but it doesn’t have to be. A macro is basically a pre-processor for code or a type that can be called from anywhere and has access to the entire scope. Needless to say, the implications of this can be pretty significant .
string macros
One high-level implication of macros is the ability to pre-process types before they are provided to the users. Considering how many structures in programming are often composed from a String it makes a lot of sense to utilize this process those types of strings straight into their type, rather than having to call some parse function — for this, Julia uses String macros. To create a String macro, we simply provide _str after a macro.
macro css_str(x::String)
kvalues::Vector{SubString} = split(x, ";")
Dict{Symbol, String}(begin
splt = split(kval, ":")
Symbol(splt[1]) => string(splt[2])
end for kval in kvalues)::Dict{Symbol, String}
endWith this new css_str macro, we provide the former part of this macro to the beginning of our String .
css"border-radius:5px;color:red;width:500px"
Dict{Symbol, String} with 3 entries:
:color => "red"
Symbol("border-radius") => "5px"
:width => "500px"inner constructors
There are a number of things that are important to consider when creating your own type system in Julia. One of the most vital aspects to an effective Julia constructor when creating concrete type is the inner constructor. An inner constructor in Julia takes some data and translates it into the Type by constructing its fields. The reason why inner constructors in particular are so important is because they are concrete — while we can extend outer constructors and change their nature, and our users can, we cannot. Considering this we certainly want to create versatile inner constructors. For this example, I will be showing the Algebra constructor from my work-in-progress project Algia — as it has a good balance of complexity and simplicity:
abstract type AbstractAlgebra end
length(a::AbstractAlgebra) = a.length::Int64
shape(a::AbstractAlgebra) = (a.length)::Tuple{Int64}
mutable struct Algebra{T <: Any, N <: Any} <: AbstractAlgebra
pipe::Vector{Function}
length::Int64
Algebra{T, N}(f::Function = x -> 0, length::Int64 = 1, width::Int64 = 1) where {T <: Any, N <: Any} = begin
funcs::Vector{Function} = Vector{Function}([f])
new{T, N}(funcs, length)::AbstractAlgebra
end
Algebra{T}(f::Function = x -> 0, length::Int64 = 1, width::Int64 = 1) where T <: Any = begin
Algebra{T, width}(f, length)::AbstractAlgebra
end
Algebra{T}(f::Function, dim::Tuple) where T <: Any = begin
if length(dim) == 1
Algebra{T}(f, dim[1], 1)
elseif length(dim) == 2
Algebra{T}(f, dim[1], dim[2])
end
end
end
function Algebra(vec::Vector{<:Any})
T = typeof(vec).parameters[1]
Algebra{T, 1}(length(vec)) do e
vec[e]
end::AbstractAlgebra
end
function Algebra(vec::Matrix{<:Any})
T = typeof(vec).parameters[1]
Algebra{T}(size(vec)) do e
vec[e]
end::AbstractAlgebra
end
const AlgebraVector{T} = Algia.Algebra{T,1}Here we see how I personally like to lay out my constructors. The inner constructors are the main constructors, which the outer constructors utilize to construct the type from other forms of data. The result is a well-organized type and subsequent set of construction methods for that type. Using outer constructors and inner constructors in this way, we get some pretty high-level syntax using type parameters that make constructing the type with a parameter or from a specific data-type is incredibly easy.
extending operators
The last technique I wanted to talk about in this article is the ability to extend Julia’s operators. While we saw one use case under constants, there are of course a lot of other high-level use cases for operators. Using operators, we could effectively make a symbolic syntax for anything — which is a pretty awesome feature of the language. The one stipulation here is that bitwise and unary operators remain bitwise or unary. In other words, if our operator takes one argument it cannot take two arguments.
import Base: +
# python string concatenation
+(s::String, s2::String) = *(s, s2)This rather simple example just makes + the String concatenation operator for Julia like it is in Python, but of course there are a multitude of different operations that can be done! This is one of the best things about Julia: the language molds around you and what you are deciding to do with it. I think it is easy to see why there are a myriad of syntax capabilities when it comes to the language.
conclusion
Julia is an incredibly enjoyable language to program in, and one thing that certainly contributes to this case is the many different capabilities the language has on the front of high-level syntax. Comparing Julia’s syntax to many other languages, while the language often looks like Python, it is clearly more type explicit and at times can represent high-level syntax a lot more easily. Admittedly, it was challenging to learn all of these things over the years, especially at first when the documentation and information on the language was a lot less mature. Fortunately, I am now able to share this with you — and that makes me happy. Thank you for reading!




