Note: The materials of the this introduction are borrowed from [1].
As a tradition, we write “Hello, World!” as our first program, which simply display the worlds “Hello, World!”. In Julia, it looks like this:
println("Hello, World!")
Hello, World!
In Pluto, an alternative solution to that is:
Text("Hello, World!")
Hello, World!
Julia provides operators, such as +
,-
,*
,/
, which respectively performs addition, subtraction, multiplication, and division:
40 + 2
42
43 - 1
42
6 * 7
42
84 / 2
42.0
You can also combine these operations, as is seen in the following example:
(2 + 4) * 7
42
Here is a table of arithmetirc operations. Note that Julia support Unicode characters like '÷
'. | Expression | Name | Description | | :–- | :–- | :–- | | +x
| unary plus | the identity operation | | -x
| unary minus | maps values to their additive inverses | | x + y
| binary plus | performs addition | | x - y
| binary minus | performs subtraction | | x * y
| times | performs multiplication | | x / y
| divide | performs division | | x ÷ y
| integer divide | x / y
, truncated to an integer (equivalent to div(x,y)
) | | x \ y
| inverse divide | equivalent to y / x
| | x ^ y
| power | raises x
to the y
th power | | x % y
| remainder | equivalent to rem(x,y)
| (exerpted from https://docs.julialang.org/en/v1.9/manual/mathematical-operations/#Arithmetic-Operators)
Operator precedence
The order of the evaluation depends on the operator precendence. Such precendences of some basic operations are given as follows, and the acronym PEMDAS is a useful way to remember the rules.
parenthesese: ()
.
exponentiation: ^
multiplication and division: *
and /
addition and subtraction: +
and /
For a complete list, check out here.
We should compare two numbers by using the comparison operators. For example, we can check whether two numbers are the same (resp., different) by using ==
(resp., !=
):
1 == 2
false
3 != 3
false
We can also do chaining comparison:
1 < 2 != 5
true
Here is a table of the comparison operators: | Operator | Name | | :–- | :–- | | ==
| equality | | !=
, ≠
| inequality | | <
| less than | | <=
, ≤
| less than or equal to | | >
| greater than | | >=
, ≥
| greater than or equal to | (exerpted from https://docs.julialang.org/en/v1/manual/mathematical-operations/#Numeric-Comparisons)
Similar to the arithmetic operators, there are some operators for strings. The operator *
concatenate two strings and ^
repeat a string for several times:
"abc" * "def"
"abcdef"
"abc" ^ 4
"abcabcabcabc"
To compare logic, we need Boolean operators. The result will be either true or false.
(2 > 1) && (3 > 2) # (2 > 1) and (3 > 2)
true
Here is a table of Boolean operators: | Expression | Name | | :–- | :–- | | !x
| negation | | x && y
| short-circuiting and | | x \|\| y
| short-circuiting or |
A value is one of the basic things a program works with, like a letter or a number. For example, 2
, 42.0
, "Hello, World!" are values. These values belong to different types: 2
is an integer, 42.0
is a floating-point number, and "Hello, World!" is a string. Check out the types of these by the command typeof
typeof(2)
Int64
typeof(42.0)
Float64
typeof("Hello, World!")
String
Matrices and vectors are also built-in types:
typeof([1 2 3 4 ; 5 6 7 8])
Matrix{Int64} (alias for Array{Int64, 2})
typeof([1, 2, 3, 4 ])
Vector{Int64} (alias for Array{Int64, 1})
Julia supports also complex numbers. Find out what types they are:
typeof(1+2im)
Complex{Int64}
typeof(1.0+2im)
ComplexF64 (alias for Complex{Float64})
There are also two special values extending the idea of numbers: NaN
(not a number) and Inf
(infinty). Check out what types they are:
typeof(Inf)
Float64
typeof(NaN)
Float64
Read also here to learn more about integers and floating-point numbers and here about complex numbers in Julia. Also, read also here to learn about how to compare NaN
, Inf
, and other regular numbers.
Note
For the type Int64
, the number 64 indicate it takes 64 bits to store such a number. It could be verify by the function sizeof
, which returns the number of byte (1 byte = 8 bits) of the argument.
An assignment statement creates a new variable and gives it a value. For example, we can define variables 🐢
and message
as below:
🐢 = 3.2
3.2
message = "a message"
"a message"
Rules of variable names
It cannot begin with numbers.
Lowercase letters are preferred as a convention.
The underscore character, _, is often used in names with multiple words, such as your_name
.
Unicode characters are allowed.
It cannot be Julia's reserved keywords, such as struct
.
What if we want to add 1 to x
? We can simplify the expression x = x + 1
by x += 1
. (To understand let...end
block, read also this.)
let
x = 0
x += 1
end
1
The pattern +=
also works with other operators above, e.g. -=
, /=
, etc.
A function is a named sequence of statements that performs a computation. After defining a function by specifying its name and sequence of statements, you can “call” the function by name. We have seen an example of a function call in our “Hello, World!” program:
println("Hello, World!")
In this program, println
is the name of the function, and the string "Hello, World!"
that we “pass” in the function is called an argument.
Most of the familiar mathematical functions are available:
sin(pi)
0.0
log10(exp(1))
0.4342944819032518
Check out the documentation to learn more about the basic mathematical functions.
In addition to calling available functions, we can also define our own! The basic syntax of a function definition is as follows:
function function_name(argument1, argument2, ...)
statements1
statements2
...
end
Roughly speaking, the variables defined in the function are local (stays alive only in the function.) The following is a function printing the string “Nothing”.
"""
`function nothing()`
The function print the string "Nothing".
"""
function nothing()
Text("Nothing")
end
nothing
nothing()
Nothing
The function nothing
performs an action but does not return a value. Such a function is called a void function. We may also define a function that returns a value, and here is an example of a function calculating the area of a circle with radius r
"""
`function circle_area(r)`
The function computes area of a circle with radius `r`.
"""
function circle_area(r)
return pi * r^2
end
circle_area
circle_area(2)
12.566370614359172
An alternative way of defining a function is the following:
circle_area_inline(r) = pi * r^2
circle_area_inline (generic function with 1 method)
circle_area_inline(2)
12.566370614359172
to check conditions and change the behavior of the program accordingly, we need the if
statement, whose basic syntax is as follows:
if condition1
execute_statements1
elseif condition2
execute_statements2
else
execute_statements3
end
The above code executes execute_statements1
if condition1
is true
, and execute_statements2
if condition1
is false
but condition2
is true
, and execute_statements3
if both condition1
and condition2
are false
. Note that else
and elseif
are optional, and there can be as many elseif
's as you want between if
and else
. See the following for examples.
begin
if 0 > 0
Text("1 is positive")
end
end
begin
if -1 > 0
Text("-1 is positive")
else
Text("-1 is negative")
end
end
-1 is negative
begin
if NaN == 0
Text("NaN is 0")
elseif NaN > 0
Text("NaN is positive")
elseif NaN < 0
Text("NaN is negative")
else
Text("NaN is neither positive nor negative")
end
end
NaN is neither positive nor negative
We can add the first n
natural numbers using if
statement.
"""
`function fibonacci_recursion(n::Int)`
The function computes `n`th term in the Fibonacci sequence.
"""
function fibonacci_recursion(n::Int)
if n <= 0
return 0
elseif n < 3
return 1
else
return fibonacci_recursion(n-1) + fibonacci_recursion(n-2)
end
end
fibonacci_recursion
fibonacci_recursion(7)
13
The above function calls itself is recursive, and such a function is called a recursion.
Note
A infinite recursion (or a never-ending recursion) would be interrupted by Julia by reporting an error message when the maximum recursion depth is reached.
The while
statement is used to repeatedly perform operations until a condition is no longer satisfied. The basic syntax of a while loop is:
while condition
execute_statements
end
For loop is a basic way to perform iterative operations. The basic syntax of a for loop is:
for iterator in range
execute_statements(iterator)
end
The break
statement is used to “jump” out of a while/for loop. See the follow example for the its usage.
for i in 1:10
if i % 3 == 0
break
end
print(i, " ")
end
1 2
The continue
statement is used to “jump” to the the beginning of a while/for loop. See the follow example for the its usage.
for i in 1:10
if i % 3 == 0
continue
end
print(i, " ")
end
1 2 4 5 7 8 10
We can implement a function computing the Fibonacci sequence using the while loop.
"""
`function fibonacci_while(n::Int)`
The function computes `n`th term in the Fibonacci sequence.
"""
function fibonacci_while(n::Int)
temp1 = 1
temp2 = 1
ans = 0
while n > 0
temp1 = temp2
temp2 = ans
ans = temp1 + temp2
n -= 1
end
return ans
end
fibonacci_while
fibonacci_while(7)
13
In a similar manner, we can implement such a function using the while loop.
"""
`function fibonacci_for(n::Int)`
The function computes `n`th term in the Fibonacci sequence.
"""
function fibonacci_for(n::Int)
temp1 = 1
temp2 = 1
ans = 0
for i in 1:n
temp1 = temp2
temp2 = ans
ans = temp1 + temp2
end
return ans
end
fibonacci_for
fibonacci_for(7)
13
A Char
value is a single character and is surrounded by single quotes. In the digital world, we need encode the letters of the numerals/alphabet/... as digits. Among the encodings, ASCII standard (American Standard Code for Information Interchange) maps commonly used numerals/English letters/punctuations to integers between 0 to 127. (It is very important to for you to get familiar with some special characters in the escaped input forms, such as newline '\n'
.) On the other hand, the so-called Unicode standard aims to include many characters in non-English languages, and it coincides with ASCII standard for values between 0 and 127. Julia natively supports both encodings for its Char
value. Some examples are given as follows. You can input any Unicode character in single quotes using \u
followed by up to four hexadecimal digits or \U
followed by up to eight hexadecimal digits (the longest valid value only requires six).
'c'
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
'\u63' + 1
'd': ASCII/Unicode U+0064 (category Ll: Letter, lowercase)
'🍌'
'🍌': Unicode U+1F34C (category So: Symbol, other)
'\u1CC4'
'᳄': Unicode U+1CC4 (category Po: Punctuation, other)
'\U1F34C'
'🍌': Unicode U+1F34C (category So: Symbol, other)
Check out the the size of a character:
sizeof('🍌')
4
A string in Julia is a sequence of characters (using the UTF-8 encoding), and thus you can access every single character via the bracket operator. However, the size varies from from character to character. (Each byte is a "code unit"):
sizeof("c")
1
sizeof("\u1CC4")
3
sizeof("🍌")
4
Therefore, sizeof
does not tells the real length of the string, and not all indices are valid:
str = "\u1CC4🍌c"
"᳄🍌c"
sizeof(str)
8
str[end]
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
str[2]
StringIndexError: invalid index [2], valid nearby indices [1]=>'᳄', [4]=>'🍌'
There are different ways of concatenating strings. Apart from *
and ^
, we can also use the following methods:
string(str,"1")
"᳄🍌c1"
Constructing strings using concatenation can become a bit cumbersome, however. To reduce the need for these verbose calls to string or repeated multiplications, Julia allows interpolation into string literals using $
:
"$(str)$(1+1)"
"᳄🍌c2"
To get the real length of the string, call length(str)
:
length(str)
3
Some useful functions are firstindex(str)
, lastindex(str)
, eachindex(str)
, nextind(str, i, n=1)
, prevind(str, i, n=1)
firstindex(str)
1
lastindex(str)
8
collect(eachindex(str))
3-element Vector{Int64}:
1
4
8
Loop through the characters in a string:
for letter in str
println(letter)
end
᳄
🍌
c
The strings are compared lexicographically (according to the UTF-8 encoding) using <
, <=
, >
, >=
, ==
, !=
.
"abracadabra" >= "xylophone"
false
"Hello, Alice." <= "Hello, Bob."
true
To understand the above comparison, we note that the two strings share the same 7 code units, but the former has “smaller” 8th code unit ('A' <= 'B'
).
To extract a substring indexed by a index set idx
, we can use the slice operator array[idx]
str[[1,8]]
"᳄c"
Some useful functions are findfirst(substr,str)
, findlast(substr,str)
, findnext(substr,str,i)
, occursin(substr, str)
, in(chr, str)
(equivalently, chr ∈ str
or chr in str
), repeat(str, n)
ncodeunits(str)
8
findnext("abc", "abcabc", 2)
4:6
occursin("abc", "abcabc")
true
'a' in "abcabc"
true
Some useful functions are ncodeunits(str)
, codeunit(str,i)
codeunit(str, 2)
0xb3
One can convert a string to an array of characters via collect(str)
.
collect(str)
3-element Vector{Char}:
'᳄': Unicode U+1CC4 (category Po: Punctuation, other)
'🍌': Unicode U+1F34C (category So: Symbol, other)
'c': ASCII/Unicode U+0063 (category Ll: Letter, lowercase)
To convert it back to a string, use join(array)
:
join(['a', 'b', 'c'])
"abc"
One can parse a string using split(str, token)
split("a-+b-c", "-+")
2-element Vector{SubString{String}}:
"a"
"b-c"
split("a b c")
3-element Vector{SubString{String}}:
"a"
"b"
"c"
The strings are immutable, meaning that an existing string cannot be changed:
let
test_str = "AAA"
test_str[1] = 'a'
end
MethodError: no method matching setindex!(::String, ::Char, ::Int64)
Instead, to modify the string, we can re-assign a new string to str.
let
test_str = "AAA"
test_str = 'a' * test_str[2:end]
end
"aAA"
Like a string, an array is a sequence of values. In a string, the values are characters; in an array, they can be any type. An example could be:
cheeses = ["Cheddar", "Edam", "Gouda"]
3-element Vector{String}:
"Cheddar"
"Edam"
"Gouda"
random_items = ["spam", 2.0, 5, [10, 20]]
4-element Vector{Any}:
"spam"
2.0
5
[10, 20]
Check out what types they are:
typeof(cheeses)
Vector{String} (alias for Array{String, 1})
typeof(random_items)
Vector{Any} (alias for Array{Any, 1})
To extract a subarray indexed by a index set idx
, we can use the slice operator array[idx]
cheeses[[1,3]]
2-element Vector{String}:
"Cheddar"
"Gouda"
We can use in(element, array)
(equivalently, element ∈ array
or element in array
) to check if an element is in the array.
"b" in cheeses
false
eachindex(array)
also works for arrays.
collect(eachindex(cheeses))
3-element Vector{Int64}:
1
2
3
Loop through the array:
for item in cheeses
println(item)
end
Cheddar
Edam
Gouda
Alternatively, we can do:
for i in eachindex(cheeses)
println(cheeses[i])
end
Cheddar
Edam
Gouda
We can use push!(array, element)
or push!(array, array_src)
to add elements to array:
let
characters = ["a", "b", "c"]
append!(characters, ["start"])
push!(characters, "end")
end
5-element Vector{String}:
"a"
"b"
"c"
"start"
"end"
To delete the element from the array, one can use splice!(array, i)
to delete the ith element and to return the deleted element, and use deleteat(array, i)
to delete ith element the array and to return the updated array. Furthermore, use pop!(array)
(resp. popfirst!(array)
) to delete the last (resp. first) element of the array, which return the deleted element.
let
characters = ["a", "b", "c", "d", "e", "f"]
splice!(characters, 3)
pop!(characters)
popfirst!(characters)
characters
end
3-element Vector{String}:
"b"
"d"
"e"
Note
The exclamation mark in push!
or pop!
is conventionally meant to indicate the arguments are written inside the function!
We can sort (in ascending order) arrays using sort(array)
sort(["c", "b", "a"])
3-element Vector{String}:
"a"
"b"
"c"
We can sum an array of numbers by sum(array)
sum([1, 2, 3, 4])
10
For every binary operator like ^
, there is a corresponding dot
operator that performs entrywise operations.
[1, 2, 3, 4, 5] .^ 2
5-element Vector{Int64}:
1
4
9
16
25
"Any Julia function can be applied elementwise to any array with the dot syntax."
"Any Julia function can be applied elementwise to any array with the dot syntax."
circle_area.([1, 2, 3, 4, 5])
5-element Vector{Float64}:
3.141592653589793
12.566370614359172
28.274333882308138
50.26548245743669
78.53981633974483
Unlike strings, arrays are mutable
let
characters = ["a", "b", "c"]
characters[1] = "d"
Text(characters)
end
["d", "b", "c"]
An object is something a variable can refer to. If now we have two variables, say two strings a
and b
both are assigned "banana"
, are these two variables refer to the same objects, or are they two different copies of the same string (immutable)? To conduct an experiment, we resort to the operator ===
to test if two variables refer to the same object, and ==
to see if the variables store the same string.
let
a = "banana"
b = "banana"
(a == b), (a === b)
end
(true, true)
As opposed to it, if we replace the value by an array (mutable), the outcome is different.
let
a = [10, 20]
b = [10, 20]
(a == b), (a === b)
end
(true, false)
In this case we would say that the two arrays are equivalent, because they have the same elements, but not identical, because they are not the same object.
Note
Here is an explanation for the operator ===
For mutable values (arrays, mutable composite types), === checks object identity: x === y
is true if x
and y
are the same object, stored at the same location in memory.
For immutable composite types, x === y
is true if x
and y
have the same type – and thus the same structure – and their corresponding components are all recursively ===
.
For bits types (immutable chunks of data like Int or Float64), x === y
is true if x
and y
contain exactly the same bits.
We should note that variables a
and b
only stores the address to the object, and therefore we may assign the address in b
to a
:
let
a = [10, 20]
b = a
(a == b), (a === b)
end
(true, true)
This is where we should be more careful with mutable types, since the actually values pointed by the address might be changed without changing the address:
let
a = [10, 20]
b = a
b[2] = 40
a
end
2-element Vector{Int64}:
10
40
To copy the properly copy a
to b
, we may consider:
let
a = [10, 20]
b = a[:]
b[2] = 40
a
end
2-element Vector{Int64}:
10
20
A dictionary is like an array, but more general. In an array, the indices have to be integers; in a dictionary they can be (almost) any type.
To create a dictionary, see following examples:
dict = Dict()
Dict{Any, Any}()
begin
global dict["one"] = 1
dict
end
Dict{Any, Any} with 1 entry:
"one" => 1
dict
Dict{Any, Any} with 1 entry:
"one" => 1
Get keys using keys(dict)
and values using values(dict)
.
"one" in keys(dict)
true
"dict" in values(dict)
false
We may also loop throught the dictionary.
for i in keys(dict)
println(i)
end
one
We get entry using get(dict, element, default_return)
:.
get(dict, "one", -1), get(dict, "test", -1)
(1, -1)
Alternatively, we can initialize a dictionary by
dict1 = Dict("one" => "uno", "two" => "dos", "three" => "tres")
Dict{String, String} with 3 entries:
"two" => "dos"
"one" => "uno"
"three" => "tres"
Check out the type of the dictionary and whether it is immutable:
typeof(dict1), ismutable(dict1)
(Dict{String, String}, true)
The following is a motivation for using tuple objects. It is often useful to swap the values of two variables. With conventional assignments, you have to use a temporary variable. For example, to swap a and b:
let
a, b = 1, 2
temp = a
a = b
b = temp
Text("$a $b")
end
2 1
This solution is cumbersome; tuple assignment is more elegant:
let
a, b = 1, 2
a, b = b, a
end
(2, 1)
typeof((1, 2)), ismutable((1, 2))
(Tuple{Int64, Int64}, false)
We may return a tuple in a function:
function minmax(t)
minimum(t), maximum(t)
end
minmax (generic function with 1 method)
let
tuple = (1, 2, 3, 4, 5)
minmax(tuple)
end
(1, 5)
Functions can take a variable number of arguments. A parameter name that ends with ...
gathers arguments into a tuple. The syntax for it would be like:
function function_name(args...)
execute_statements
end
For example, the following function prints all but the first arguments passed in the function:
function printall(args...)
println(args[2:end])
end
printall (generic function with 1 method)
printall(1,2,3,4,5)
(2, 3, 4, 5)
The operator ...
also scatters a tuple to pass it into a function. The following code is an example to demonstrate this:
let
area_rectangle(x, y) = x * y
rectangle = (3, 5)
area_rectangle(rectangle...)
end
15
md"Arrays and Tuples
zip is a built-in function that takes two or more sequences and returns a collection of tuples where each tuple contains one element from each sequence. This example zips a string and an array:"
LoadError: UndefVarError: `@md_str` not defined
in expression starting at none:1
s = "abcd";
t = [1, 2, 3];
This is what it looks like if we zip s
and t
:
zip(s, t)
zip("abcd", [1, 2, 3])
typeof(zip(s, t))
Base.Iterators.Zip{Tuple{String, Vector{Int64}}}
The Zip
type is sort of an iterator, and thus we can loop through it.
for pair in zip(s, t)
pair
end
We can also use collect to make it an array:
collect(zip(s, t))
3-element Vector{Tuple{Char, Int64}}:
('a', 1)
('b', 2)
('c', 3)
Dictionaries can be used as iterators that iterate the key-value pairs. You can use it in a for loop like this:
let
d = Dict('a'=>1, 'b'=>2, 'c'=>3);
for (key, value) in d
println(key, " ", value)
end
end
a 1
c 3
b 2
You can use an array of tuples to initialize a new dictionary:
let
t = [('a', 1), ('c', 3), ('b', 2)];
d = Dict(t)
end
Dict{Char, Int64} with 3 entries:
'a' => 1
'c' => 3
'b' => 2
We can create a text file using open(filename, mode)
, write to a file using write(iostream, str)
, and close a file using close(iostream)
. The following code serves as an example.
let
fout = open("output1.txt", "w")
write(fout, "Her name is Alice.\nHis name is Bob.")
close(fout)
end
Check also the functions write
, readlines
Note
The argument filename
can be either the complete path or the relative path with respect to where the .jl
file is at.
If we do not close the file, the file will still be closed as the code exit.
To learn more about mode, please check out here. Basically,
read
means reading file;
write
means writing to file;
append
means always writing at the end of the file;
truncate
means clearing the file if existing;
create
means creating the file if not existing;
By default, the mode is r
A lot of things can go wrong when you try to read and write files. If you try to open a file that doesn’t exist or that you don’t have permission, you get a SystemError. In these cases, we can use try
-catch
statement to handle these exceptions, and finally
to execute instructions when the given block of code exits. An example is given as follows:
let
try
i = 1;
fin = open("output1.txt")
try
for line in eachline(fin)
println("line $( i ): $line")
i += 1
end
finally
close(fin)
println("File closed.")
end
catch exc
println("Something went wrong: $exc")
finally
println("The code ends.")
end
end
line 1: Her name is Alice.
line 2: His name is Bob.
File closed.
The code ends.
We listed here some basic but useful functions. To learn more about these functions, check Live Docs
in Pluto or documentation.
If we want to type a matrix 2 x 3 matrix, one way to do it is to use white space for horizontal concatenation, and use semicolon for vertical concatenation.
[1 2 3 ; 4 5 6]
2×3 Matrix{Int64}:
1 2 3
4 5 6
We note that the convention is slightly different from MATLAB. See documentation for a more detailed explanation of concatenation.
To access the element at (2,3), we type:
let
A = [1 2 3 ; 4 5 6]
A[2,3] = 7
A
end
2×3 Matrix{Int64}:
1 2 3
4 5 7
'
Use '
to transpose a matrix / vector:
[2, 3]'
1×2 adjoint(::Vector{Int64}) with eltype Int64:
2 3
[1 2 3; 4 5 6]'
3×2 adjoint(::Matrix{Int64}) with eltype Int64:
1 4
2 5
3 6
let
A = [1 2;3 4]
b = [1,0]
A \ b
end
2-element Vector{Float64}:
-1.9999999999999998
1.4999999999999998
ones/zeros
The function ones
(resp. zeros
). Generate an all-1 (resp, all-0) vector matrix
ones(3,3)
3×3 Matrix{Float64}:
1.0 1.0 1.0
1.0 1.0 1.0
1.0 1.0 1.0
ones(Integer,3)
3-element Vector{Integer}:
1
1
1
size([1 2 3 ; 4 5 6])
(2, 3)
eigvals
and eigvec
To compute eigenvalues and eigenvectors, we need to import the LinearAlgebra
module. We type:
import LinearAlgebra
LinearAlgebra.eigvals([1 2 ; 3 4])
2-element Vector{Float64}:
-0.3722813232690143
5.372281323269014
LinearAlgebra.eigvecs([1 2 ; 3 4])
2×2 Matrix{Float64}:
-0.824565 -0.415974
0.565767 -0.909377
[1] | Think Julia: How to Think Like a Computer Scientist, https://benlauwens.github.io/ThinkJulia.jl/latest/book.html. |
Every language has its own programming style (e.g. capitalization of variable names or avoid overuse of certain commands), and it is recommended to follow those coding styles are usually recommended. At the very least, it helps others in the community to read the code. Sometimes it also improves the performance of the code since it might sometimes related to how the code is compiled or executed and even how the language itself is designed.
The reason the some of the codes are encapsulated in a let
is two-fold. On the one hand, Pluto need a begin
block or a let
block to run multiple commands in one cell. On the other, let
block, unlike begin
block, create a new local scope to avoid name conflict (The x
in the block has nothing to do with the formerly defined x
). For a better understanding of the scope in Julia, please also read the documentation, from which the following table is excerpted. | Construct | Scope type | Allowed within | | :–- | :–- | :–- | | module
, baremodule
| global | global | | struct
| local (soft) | global | | for
, while
, try
| local (soft) | global, local | | macro
| local (hard) | global | | functions, do
blocks, let
blocks, comprehensions, generators | local (hard) | global, local | begin
blocks and if
blocks do not create new local scopes. In summary, we have the following rules.
If the current scope is global, the new variable is global; if the current scope is local, the new variable is local to the innermost local scope and will be visible inside of that scope but not outside of it.
When
x = <value>
occurs in a local scope, Julia applies the following rules
Existing local: If x is already a local variable, then the existing local x is assigned;
Hard scope: If x is not already a local variable and assignment occurs inside of any hard scope construct, a new local named x is created in the scope of the assignment;
Soft scope: If x is not already a local variable and all of the scope constructs containing the assignment are soft scopes the behavior depends on whether the global variable x is defined:
if global x is undefined, a new local named x is created in the scope of the assignment;
if global x is defined, the assignment is considered ambiguous:
in non-interactive contexts (files, eval), an ambiguity warning is printed and a new local is created;
in interactive contexts (REPL, notebooks), the global variable x is assigned.
It should be a good exercise to think about which rule should apply in the above examples.
Sometimes you are not looking for an exact string, but a particular pattern, say for example, date of the format YYYY-MM-DD
. Then, regular expressions just comes in handy:
begin
#' The input string containing two dates
date = "Alice's birthday is 1991-01-01, and Bob's birthday is 1992-02-02."
println("Input string: ", date)
#' Define the regex string and substitution string
regex1 = r"[0-9]{4}-[0-9]{2}-[0-9]{2}"
regex2 = r"(?<Alice>[0-9]{4}-[0-9]{2}-[0-9]{2})(.+)(?<Bob>[0-9]{4}-[0-9]{2}-[0-9]{2})"
substitution = s"\g<Bob>\2\g<Alice>"
#' Find the first matched date
m = match(regex1, date)
println("First found date: ", m.match)
#' Find the second matched date
m = match(regex1, date, m.offset + 1)
println("Second found date: ", m.match)
#' Swap two matched date
date_swap = replace(date, regex2 => substitution)
println("Swap the dates: ", date_swap)
#' Types of r"..." and s"..."
println("The type of r\"..\" string: ", typeof(regex1))
println("The type of s\"..\" string: ", typeof(substitution))
end
Input string: Alice's birthday is 1991-01-01, and Bob's birthday is 1992-02-02.
First found date: 1991-01-01
Second found date: 1992-02-02
Swap the dates: Alice's birthday is 1992-02-02, and Bob's birthday is 1991-01-01.
The type of r".." string: Regex
The type of s".." string: SubstitutionString{String}