Hello Julia?
Chapter 0: The Elegant Julia
Introduction
This is the chapter zero of my upcoming programming in Julia textbook. The digital-only book assumes no pre-requisites, but moves at a rather fast pace (no fluff, no stories or random opinions about author, no discussion of how computers work or any historical references), all the while avoiding any irrational jumps, the confusing jargon overload, the incomplete information, the flawed pedagogy, or the boring handwaving so typical in so (unfortunately) many programming books.
If you already have basic programming experience and want to go to the next level, please proceed to Ruby where the lessons are start at the advanced beginner and go all the way to the competent intermediate level.
What is this book?
This book attempts to teach ultra-beginner level programming in Julia programming language. Some books only focus on programming and the language is merely used as a vehicle to teach programming. Others merely teach the language, but assume the learner knows programming. There are many books in the latter categry, but in the former, only one or two, and they are not very beginner-friendly.
Interestingly, in the category of books that teach programming first, they do not cover enough of the vehicle language to make the book a complete pacakage. The learner is then forced to go and pick up another book on that language, but now the pedagogical continuity has been broken up, and there are no guarantees that the two books have the same teaching style, or more importantly, the same quality.
Finally, although many programming books have been written, and I have researched most of them, the programmers who write books may be excellent programmers, but they are not very good teachers. Teaching is hard work and most teachers don’t know how to teach.
Youtube is no better, either. A certain Youtuber teaches Scala but never mentions its object-functional aspects, for instance, but many people like and follow him and have no idea what’s missing (the unknown unknowns).
The pedagogical model in this book
The pedagogical model adopted by me has been refined for twenty years; this translates to nearly 30000 hours of ever-improving and battle-tested instruction experience. My pedagogical model is can be summarized as:
“Here is everything you need to know about O, this is how it is done (method Y), here is another example, and another and another. Now, here is a small problem, call it problem O. Apply the method Y to problem O. Next, we have method K. Here are several progressively-difficult examples. Now apply method K to problem OY (the O problem which has been modified after method Y was applied to it. Next, we see good and bad cases of method S. Here are examples of good and bad S. Apply a bad S and then a good S to problem OKY. How did it change? Here are the potential problems that might occur. Now let’s see topic R. Let’s apply KYS to R and see how it differs from O. And then we move forward.”
The point of the above example is to highlight the importance of creative redundancy in producing a tutorial that is patient but fast-moving, and free from errors, mistakes, or incomplete information. I have tried my best to replicate myself as a mentor who is by your side, helping you just at the right time, without either simplifying too much, or making it too cryptic to fully understand and learn.
Additionally, both the nature of the problems, and the tools at our disposal to solve them become progressively more difficult or complex, without losing the pedagogical clarity and student-centric explanation. The goal of the text is to obviate the need for a teacher. The book is the teacher.
As far as I know, there are no programming textbooks that have adopted this approach. Yet, such an approach is the only surefire way to learn programming and learn it while having fun and improving your problem-solving skills. My aim is to let the learner acquire fishing skills, and not merely show them how I fish so they can imitate me (like what they do on YT).
Solutions are given but not immediately provided. A clear location has not been given for solutions (like the end of the book), so that the learner is compelled to read through the text to discover the solutions. This prevents learner-laziness, but also promotes long-term memorization due to Zeigarnik effect.
Why Julia?
We use Julia for two main reasons: it is a good teaching language, but also an important real-world-ready new language, which boasts C-like speed, Kotlin-like safety, Ruby-like flexibility and programmer-happiness, A touch of Lispiness, Pythonesque clarity and terseness (even clearer than Python), and Matlab-like quant powers.
We adopt Julia for this text for several important reasons:
-
Julia is very easy to learn; it is flexible enough to let the user bend it to his (no bias intended) will. When you break into programming, this is very important, since you will need to unleash your creativity in solving the problems, and the language should facilitate this. Julia does this very well.
-
Julia is very elegant and beautifully-designed. It is a joy to write and a joy to read. By contrast, Python is ugly, and Java is even uglier. Other elegantly-designed languages are Kotlin, Ruby, the ML family of languages, and J (but for J, some people might disagree with me.).
-
Julia is very similar to many languages, so learning Julia will help you learn those other languages faster and better. Julia is syntactically similar to Python, Ruby, Kotlin, and better yet, you can call (directly use) Python, C, and R inside Julia.
-
Julia is new and designed for the future of computing. Although its adoption rate is not too wild, nor it is as popular as Python, it is growing rather fast, and with time, may become an important mainstream language.
-
For data science, statistics, analytics, ML and AI, Julia is already making a name for itself. It is only a matter of time before it is adopted for other niches. The main reason is that it is super-fast, extremely-flexible, beautifully-designed, easy-to-work with, and powerful enough to justify learner effort. As you finish this book, you will begin to appreciate how Julia is so much better than many existing–but popular–programming languages.
Popularity is necessary but not sufficient. There are languages that are very popular but their popularity has been accidental, and due to network externalities, for instance. From the point of view of a beginner, at least, Julia is a language that will be worth its potential popularity.
We will now directly proceed.
Why is Julia so beautiful?
Suppose I want to write a “verb”, which we call a function in programming, that returns (gives us) a greeting. In Julia, this is how we do it:
function greetbot()
return "how do you do?"
end
So, function
is a reserved keyword that is used to construction a procedure in Julia. The name greetbot
I chose can be anything that is clear and suitable to the task. It must be followed by ()
so as to give the impression that this is a function, a verb, a procedure. In many languages, this is necessary.
And return
is another reserved keyword. It is used in the body of the function to give us back the outcome of the operation that occurs in the body of the function.
If you want to speak human language to the computer, you better use “I am some text” so that the computer knows this is not a number, or a logical value such as true
or false
. So our greetbot, in its body operation, takes some text–text is called string in programming–in this case “how do you do?” and gives it back to us each time we ask greeetbot() to do its job.
Installation
Please skip this part if you know how to install Julia.
The program greetbot gives us the greeting message when we call the function. This means we write it down in the terminal. Basically a terminal is a program designed to run programs–and do many other things, of course. You can find the terminal on Linux and Windows as well. For programming beginners, I highly recommend the MacOS: on a Mac, search for it in the spotlight and the program will load.
To install Julia, simply download homebrew on your Mac, and then instal Julia without pain. Briefly, this is how:
Go to https://docs.brew.sh/Installation. On your Mac Terminal, switch your terminal from zsh to bash (or else it won’t work!) by typing:
exec bash
Then press enter
and now you are ready to install homebrew from the above link. After that is done, launch your terminal and type this:
brew cask install julia
If this method failed, or somehow you could not do it, an alternative is to download Julia for your OS, and on simply double-click to install it (for MacOS). For other operating systems, a detailed explanation is given here.
Proceed here if you already installed Julia
So now, Let’s see the greetbot in action. If you are viewing this on a mobile phone, tilt your phone horizontally so that the font looks bigger:
Now we are ready to do more. You have perhaps heard of something called the Fibonacci sequence. If not, don’t panic because we will get to the bottom of it. Suppose I show you a line made of this numbers placed in a sequence:
0, 1, 1, 2, 3, 5, 8, 13, 21, 34...
What is going on? Suppose we take the first five numbers:
As we go from 0 to 1, we are moving from the 1st number to the 2nd number. As we go from 1 to 1, we are moving from the 2nd, to the 3rd numbe. Sequences emerge when a pattern emerges. The first part of the data is not sufficient to tell us whether a sequence is emerging. The second part gives us a bit more information, but it is still not enough. We cannot confidently answer how we went from 1 (in the second position) to another 1 (in the third position). We need more information.
Looking at 2, in the 4th position, we can conjecture: if we add 0 + 1, we get the third-position 1, and if we add 1 + 1, we get the fourth-position 2. The pattern is emerging, but we are not confident yet. We need more data.
If we continue to apply this pattern, it matches the results above. So we can define the Fibonacci sequence as a sequence of numbers who are sums of the previous two numbers. Is this definition clear enough?
Now, we would like to create a function that generates the Fibonacci sequence. Don’t worry we will start very slowly and will not make irrational jumbs (a jumb is a sudden jump in difficulty levels causing the learner to feel they are dumb–which, in fact, they are not! Also, it may take a while before we come back to the Fibonacci discussion.).
In any programming language, the concept of the conditional is something that involves if
and else
. If food is cooked, turn off the oven, otherwise (else), keep cooking. Very straight-forward. Now we wet our feet by creating a function that takes a parameter (an input) and does something to that input. Suppose yo want a function that takes a number and doubles it. In Julia:
function doubler(h)
return h * 2
end
Note that the input can be any number (but no text or strings) and we called it h
but we could have called it anything. This does not matter so long as the parameter name is clear. Well, h
is not very clear. We could have done better. Now you may go to www.repl.it to launch an online Julia scratchpad, or use your local Julia installation (best to work locally). Your task is to implement (write, create) a function that takes a number, adds it to 13, multiples it to 6, divides it over 2. Go ahead and do this now before looking at our solution, which is given somewhere below (in order to prevent potential laziness ;)).
Another thing is that you are not limited to passing only one parameter to a given function. Our first example showed that a function can be parameter-less. The second example stated that it can take one parameter. But we can also do n parameters. As an example, let’s create a function that takes 3 numeric parameters. If the first param is larger than the other two, it should multiply all three together; otherwise, it should add them all up.
I will now demonstrate this so you can see the conditionals in action inside a function body. This does not mean conditionals can only be used within a function body. They can be used anywhere. Our function is:
function mathurator(a, b, c)
if a > b && a > c
return a * b * c
else
return a + b + c
end
end
Quite a lot is going on here, so let’s dissect them. We added a custom name mathurator
to our function. The function takes three parameters, which we temporarily labeled a
, b
, c
. Params must be separated by a comma.
Then we added our first conditional clause. We joined the two conditions by an AND, which is &&
in Julia. In many other languages it is and
or &
and so forth. The second conditional clause is activated as long a the first clause is not true. So if a is not bigger than b and c at the same time, then the else
clause will activate and the params will be summed.
Unlike Python but like Ruby, Julia requires an end
to create visual order in the code structure. So one end
is required to end the conditional if-else
siblings. Another end
is needed to signal closing the function off. The indentation is optional and a Julia program can run even if you don’t indent them like this.
A two-space indentation is a matter of standard style and good code readability, so you should follow the standard style because then your code will look easy on the eyes.
Let’s see this in action:
Once again, if you look carefully, quite a lot is happening and we need to see exactly what. The gif is on auto-loop for a pedagogical reason. I will make an explanation here, and then you, the good and intelligent detective, have to watch the looping gif to discover which part matches which part of my explanation.
In animation, you should see the following things happening:
- We are defining our function and call the function with three params, where a is larger than b and c. It works and gives us the result.
- Next, if we introduce an additional
if
in the conditional clause, our first param, which is: a is not bigger than b and c should work but does not. It gives us nothing. It refuses to cooperate. - We have simply asked: if a is bigger than b && if a is bigger than c ? The error message is very clear. It tell us the additional
if
needs its own correspondingend
. But even after we add the missingend
, the computation does not happen. Why introducing an additionalif
caused an error, and do we need this extraif
?
Now, before we launch into this discussion, let’s see the solution to your first task: a function that takes a number, adds it to 13, multiples it to 6, divides it over 2:
function mathbroth(n)
return (n + 13) * 6 / 2
end
Computing is a precise topic. The definition of our task was not precise enough. It did not state: “takes a number, first adds it to 13, and then multiplies the outcome to 6”, which is what I have shown above. Because of that ambiguity, we could have also done this:
function mathbroth(n)
return n + 13 * 6 / 2
end
Which would have given us a totally different number because Julia follows the mathematical order of precedence: first multiplication and division are done, then addition and subtraction. Go ahead and try implementing in both ways and pass the same input to see what kinds of different results you will get.
Now we are back to the mathurator
discusson. Whenever you have a computing problem, try to break it down to elementary bits and see if they too have any problems? If something works at the elementary, or atomic level, then this provides clues to fix our bigger problem. At the very least, it points us into the direction of a potential solution.
Hence, we are asking why mathurator
does not work as expected when a is less than b and c. What is causing the issue? We simplify it to two parameters and see that it works:
Clearly, the if-else
is working when we use two params. Then the problem must occur when the third param is added. Specifically, the problem occurs because of how we joined two conditionals with &&. What do you think should we do to resolve it? This is your task II. Play with it until you figure it out. Of course, the solution is given somewhere below, so if you got stuck you can be guided back to the land of the unstuck.
Now we will turn our attention to an interesting aspect of programming in general, and Julia in particular. As you can probably imagine, an idea can be expressed in many different ways–not always, though–but in general.
A good example of this is how a foreign text can be translated into English. The essence of the text is still the same, but the translatons slightly vary; the variation creates or imparts a difference that reflects the style, purpose, background, and personality of the translator.
Likewise, when you program, depending on the factors I just mentioned, it is possible to solve a given problem in a number of ways, some of them more elegant the rest. Julia is very flexible and allow many possibilities–as does Ruby, another grerat language. Some languages do not allow this, though. Golang or Go is “boring”, in the sense that code written by someone else can be read and easily understood by anyone proficient in Go.
When many people work together–teams–languages like Go are useful because they allow new people take over the legacy or old programs written by others. But programming is not always about teams, so Julia shines when it comes to this flexibility of options. We will show this in a moment, but before I forget, let’s look at the solution of the if-conditional mentioned above:
function mathurator(a, b, c)
if a > b && a > c
return a * b * c
else
return a + b + c
end
end
mathurator(1, 2, 4)
> 7
The answer is that if it ain’t broke, don’t fix it. A programmer should value conciseness and clarity, but not the former at the expense of the latter. In other words, programs should, first of all, work, and then be clear; if possible, also concise.
Introducing an additional if
does not add anything to the program. It even detracts from it by causing additional problems. Now that we have a working solution that is good enough, can we make it better?
By better, I mean can we make it more concise somehow? A big part of learning is guessing, and you are welcome to try it before you see my solution below. Ask: can you eliminate some stuff or regroup them to make the code shorter?
How about:
function mathurator(a, b, c)
if a > b && c
return a * b * c
else
return a + b + c
end
end
The code works and it looks better. We are saying: if a is larger than both b and c, which the pithy &&
can do very well.
Before I ask you to write the above function in one line(!), I would like to show you a simpler example which can be extrapolated to our problem. Suppose you have a function that takes two inputs (params), and adds them:
function adder(x, y)
x + y
end
This is all good and well, but Julia can do better:
adder(x, y) = x + y
We have removed the keyword function
and the final end
, and used =
to refer the function body to the function definition (that is, to tell adder
what it must do when it receives x and y).
Another piece of the puzzle is what is called the ternary; at this point, don’t worry about what it means or why it is called that. In brief, it is a shorter way to do the if-conditional. In Julia, it goes like this:
If a is bigger than b, add them together, otherwise multiply them:
doer(d, z) = (d > z) ? d + z : d * z
we defined a function called doer
which takes d and z as params or arguments. Next, we say: if d is more than z ? add them, otherwise (:) multiply them.
In plain Julia, this would be:
function doer(d, z)
if d > z
return d + z
else
return d * z
end
end
Which one is better? Such preferences can be personal, sometimes. In general, code should be readable and clear, even if it is slightly longer. But it is terse yet clear and readable, that’s fine, too. What do you think? which one is more readable?
Now you know how to define one-line functions and also do ternary operation within a one-line function. Given your knowledge, convert the following function with which you have already become familiar to a one-line ternary form:
function mathurator(a, b, c)
if a > b && c
return a * b * c
else
return a + b + c
end
end
While you do this, and a solution will be provided later, let’s turn our attention to a slightly more involved problem. The essence of programming is problem-solving. The first step is to understand exactly what the problem is, and then listing the steps that are needed to solve it.
As a good example, let’s see the following problem:
Create a function that takes in an integer (a number) as an argument. The number provided will always be a year. The purpose of the function is to report whether the year entered into it is a leap year or not.
The formula for computing a leap year is: if a year is divisible over 4, and at the same time is not divisible over 100, or is divisible over 400, then it is a leap year. In other words:
A year must be divisible over 4: condition 1. A year must not be divisible over 100: condition 2. A year must be divisible over 400: condition 3.
A year is a leap year when cond 1 && cond 2 OR cond 3 is true. In Julia, logical OR is ||
. So now, you have all the ingredients and you know how to make functions. Go right ahead and write this function however you like.
While you are doing this, here is the solution to the previous mathurator
function in ternary one-liner:
mathurator(a, b, c) = a > b && a > c ? a * b * c : a + b + c
Try to tweak it a bit. What else can we do to it to make even shorter? The following is the result. Can it be made even shorter?
mathurator(a, b, c) = a > b && c ? a * b * c : a + b + c
It appears not. This is the best we can do, at this stage at least. Now, let’s take a look at several different solutions to the leap year problem. Your job as a programmer is not only writing code, but also reading it. To read and to get comfortable understanding code is an essential technical skill.
The following function is very straight-forward but borrows its style from the imperative programming (IP) paradigm. In IP, code is a series of step-by-step instructions:
function is_leap_year(year::Int)
if year % 400 == 0
return true
elseif year % 100 == 0
return false
elseif year % 4 == 0
return true
else
return false
end
end
The only new thing here for you is the ::Int
which is telling the function to expect an integer (an integer is a counting or whole number, like 1, 2, 3, 4 as opposed to a real number or a float which is also called a decimal, like 1.0, 1.1, 4.5 and so forth).
In Python and some other so-called dynamic languages, there are no options to specifically specify the type of the data. Julia is best of both, since it can be dynamic–you need to specify the type, or if you fear errors or want to ensure nothing goes wrong, you can specifically instruct your Julia function to only take in the Int type.
If you now supply a string or a float, it will throw an error (tell you it fails and why):
The error report is very clear. It says, on line 1, your input type is String (text) or Float (decimal), whereas it specifically tells you to supply the function with Int types for this to work. As an exercise, try to remove the explicit requirement in the function definition. See what kind of error messages you get when you pass in a string or a float, when no input type is specified.
Now, let’s look at another solution for the isleap
problem:
function isleap(year)
leap_year = false
if divisible_by(year, 4)
leap_year = true
if divisible_by(year, 100)
if divisible_by(year, 400)
leap_year = true
else
leap_year = false
end
end
end
return leap_year
end
function divisible_by(year, denominator)
return (year % denominator) == 0
end
What is so interesting about this solution? First of all, it is way longer than it should have been. Problems like these occurr due to programming-language influence: the style and constraints of your first programming language (or the language you used most often) tend to train you to think in a certain way, so that when you learn a new language, you will write code in the new language, the way you were writing in your first language.
The above solution is symptomatic of the Java/Javascript style of languages: we are beating around the bush to finally come to the point. For one, a new function divisible_by
is given (anywhere in the program body–this occurs often in Javascript, for instance); then this function is picked up by the main isleap
function, which uses it in its body, to compute leap year conditions.
Another thing worth noting is the idea of nested conditionals which you can see in this java-esque code. The first if
nests another if
inside its scope, or belly, which can be confusing to beginners. This is nothing but the same as saying if cond1 && cond2
.
Let’s isolate it to study it:
if divisible_by(year, 4)
leap_year = true
if divisible_by(year, 100)
if divisible_by(year, 400)
leap_year = true
else
leap_year = false
So if a year is divisible by 4, return true
–it is a leap year, or what we called cond1
. At the same time, if a year is divisible by 100, return false
, our cond2
. But within the cond2
there is still another clause: among the years which are divisible by 100, if any such year is divisible by 400, then it is still a leap year. We call this cond3
. So we are basically saying:
if cond1
return true
if cond2
if cond3
return true
else
return false
This is okay-ish, but it is too long and we are better off using logical operators to clarify intercode relations. The following code makes an attempt to use logical operators to simply the logic of the program:
function isleap(year::Integer)
if divis_by(year, 4)
!divis_by(year, 100) || divis_by(year, 400)
else
false
end
end
divis_by(n::Integer, div::Integer) = n % div == 0
It resembles the above Java-esque code in some ways. An auxiliary function, divis_by
has been created to handle the condition check. If a year is divisible by a certain number, then the remainder must be zero. For instance, if 14 is divided over 4, the result is 3 and the remainder is 2. Thus, 14 is not divisible by 4, but 15 is divisible over 5.
Also, in Julia, it is possible to declare input type as ::Integer
instead of ::Int
which is not a big deal, either way is fine, and I prefer the latter. Further, we see a !
which is a logical operator representing not. So the following line says:
!divis_by(year, 100) || divis_by(year, 400)
Not divisible by 100 OR divisible by 400. Either this, or that. But not both.
This is much better, but can we do better yet? Let’s see the following:
function isleap(y)
if (mod(y, 4)== 0) && ( mod(y, 100) != 0 || mod(y,400) == 0)
return true
else
return false
end
end
This new code is making extensive use of logical operators and uses parentheses to group the conditionals for better readability. It also uses mod(y, n)
, which is an in-built Julia function that does away with the divis_by
function we had seen earlier.
The code is good enough, but there are way too many repititions. Also, we have to guard against overusing the in-built functions, or else we will have something like this. See if you can guess what it does:
function isleap(y)
if rem(y, 400)==0 && rem(y, 100)==0 && rem(y, 4)==0
return(true)
elseif rem(y, 100)==0 && rem(y, 4)==0
return(false)
elseif rem(y, 4)==0
return(true)
else
return(false)
end
end
Like mod(number, divisor)
, rem(a, b)
checks what is the remainder of dividing a over b. The above code is an example of overkill. Being unnecessarily verbose for no reason at all. You can in fact do away with the mod
and rem
functions altogether:
function isleap(y::Int64)::Bool
if (y % 4 == 0)
if (y % 100 == 0)
return (y % 400 == 0)
else
return true
end
else
return false
end
end
We introduced a ::Bool
on the first line to indicate the return type of the function. That is, we are instructing our function that the final result this function returns must be of the Boolean type, that is, either true
or false
.
Also, we specify ::Int64
which is like ::Int
but contains way more numbers. A simple Int
includes all the numbers between -2147483648 and +2147483647. However, sometimes you may need a larger range of numbers. If you are buying potatoes, Int (also called Int32
suffices. If you want to do inter-galactic compuations, using Int will result in errors, so we need a more powerful Int, such as Int64
. The range for the latter is a whopping: –9,223,372,036,854,775,808 and 9,223,372,036,854,775,807.
In the context of the above problem, using Int64 is an overkill. Aside from that, in-built mod(a, b)
has been done away with, but nested conditionals are used instead of logical operators. A better version is:
function leap_or_not(y::Int)
return (y % 4 == 0) && (y % 100 !=0 || y % 400 == 0)
end
Which I personally like and would accept as my own style. Can we use the ternary operator here? Sure we can. Your next exercise is to convert my favorite function leap_or_not
so that it contains the ternary operator.
Also note that our leap_or_not
can be expressed as a one-liner function:
isleap(y::Int) = y % 4 == 0 && (y % 100 != 0 || y % 400 == 0)
Which is also neat. As a second exercise, convert your ternary-including function to a one-liner ternary. While you are at it, let’s see two more examples.
Earlier I mentioned that white space is meaningless in Julia. As an instance, see the following which also works:
isleap(year::Int) =
year % 4 == 0 && (
year % 100 != 0
|| year % 400 == 0
)
Now let’s see that ternary version. By the way, ternary operators can be used more efficiently, like so:
function isleap(y::Integer)
y % 4 == 0 ? y % 100 == 0 ? y % 400 == 0 ? true : false : true : false;
end
Notice the semi-colon at the end of the ternary declarations. This is another artifact from Javascript where then end of each line of code is marked with a semicolon. In Julia, we don’t need that.
Also, although there are three conditions, four booleans are given. The first bool is for y % 4 == 0 ?
which evaluates to true
. The second is otherwise false if y % 100 == 0 ?
, but within that, true if y % 400 == 0 ?
but otherwise, yeah, false
. Try removing the last false
and see what happens.
Here is another ternary version without the semicolon, and slightly re-arranging the ternary declarations for better readability:
function isleap(y)
y % 400 == 0 ? true : y % 4 == 0 ? y % 100 == 0 ? true : false : false
end
Speaking of one-liners, here is yet another way to do the one-liner using logical operators and something you have not seen before:
function isleap(y)
y-> (y % 4 == 0 && (y % 100 != 0 || y % 400 == 0))
end
We will discuss ->
in future lessons, but for now, the code can be understood intuitively. That explains why Julia is beautiful, like Ruby: when you see something new, you can readily guess what it does.
now, let’s see our solution to the one-liner ternary-based clearly-written function that does away with all the bells, whistles and frills without hurting readability:
isleap(y) = y % 4 == 0 && y % 100 !=0 || y % 400 == 0 ? true : false
A long way from that Java-esque code, isn’t it?
This marks the end of chapter zero. Now you are ready to enjoy the whole book. The book is being written as you read this, but you are welcome to check back here or on my Quora, to see when it will be available.