<function>
or <operator>
).
This is just notation, and the symbols <
and >
should not be misconstrued as Julia's syntax.
Action | Keyboard Shortcut |
---|---|
Previous Section | Ctrl + ๐ |
Next Section | Ctrl + ๐ |
List of Sections | Ctrl + z |
List of Subsections | Ctrl + x |
Close Any Popped Up Window (like this one) | Esc |
Open All Codes and Outputs in a Post | Alt + ๐ |
Close All Codes and Outputs in a Post | Alt + ๐ |
Unit | Acronym | Measure in Seconds |
---|---|---|
Seconds | s | 1 |
Milliseconds | ms | 10-3 |
Microseconds | ฮผs | 10-6 |
Nanoseconds | ns | 10-9 |
Variable scope refers to the code block in which a variable is accessible. The concept allows us to distinguish between global variables, which are accessible in any part of the code, and local variables, which are confined to specific blocks like functions or loops. The existence of scopes determines that the same variable x
could refer to different objects, depending on where it's called.
When it comes to functions, Julia adheres to specific rules for variable scope. Specifically, given a variable x
defined outside a function:
if a new variable x
is defined inside a function or is passed to a function as an argument, then x
is considered local to that function. This means that any reference to x
within the function refers to the local variable, without any relation to the variable x
defined outside the function,
if a function doesn't define a new x
nor x
is a function argument, then x
refers to the variable defined outside the function (i.e., the global variable).
In this section, we'll show how these rules work in practice.
A variable that is local to a function exists solely within that function's scope. This means that these variables cease to exist once the function finishes executing. Consequently, any attempt to reference local variables outside the function will result in an error.
Variables local to a function encompass:
the function arguments,
the variables defined in the function body.
Any other variable included in a function that's not i) or ii) necessarily refers to a global variable.
Understanding which variables are local or global is essential for predicting a programโs behavior. This is because a local variable may share the same name as a global one, without them being related. The following examples help clarify the differences between global and local variables.
x = "hello"
function foo(x) # 'x' is local, unrelated to 'x = hello' above
y = x + 2 # 'y' is local, 'x' refers to the function argument
return x,y
end
foo(1)
x
y
z = 2
function foo(x)
y = x + z # 'x' refers to the function argument, 'z' refers to the global
return x,y,z
end
foo(1)
x
z
In programming, functions can be understood as self-contained mini-programs to represent specific tasks. Under this interpretation, local variables simply act as labels that help articulate the mechanics of the task. Consequently, their inaccessibility outside the function emerges naturally. [note] Local variables play a similar role to integration variables in math. Formally, in \(\int f\left(t\right)\,\mathrm{d}t\) for some function \(f\), \(t\) just represents a symbol indicating over which variable we're integrating. The integral could be equivalently expressed using any other integration variable, such as \(x\) in \(\int f\left(x\right)\,\mathrm{d}x\).
To explain this view of functions, consider a variable x
, along with another variable y
computed by transforming x
through a function f
. In particular, assume a transformation that doubles x
, so that y = 2 * x
. The following are two approaches to calculating y
.
x = 3
double() = 2 * x
y = double()
x = 3
double(x) = 2 * x
y = double(x)
x = 3
double(๐) = 2 * ๐
y = double(x)
The function in Approach 1 relies on the global variable x
. This practice is highly discouraged for several reasons. Firstly, it prevents the reusability of the function, as it's specifically designed to double the global variable x
, rather than acting as a mini-program that doubles any variable.
Second, the inclusion of the global variable x
compromises the function's self-containment, as the function's output depends on the value of x
at the moment of execution. If you work on a long project, this will turn the code prone to bugs.
Lastly, global variables have a detrimental impact on performance, a topic we'll study later on the website. In fact, global variables in Julia are directly a performance killer.
In contrast, Approach 2 refers to x
as a local variable. This x
is unrelated to the global variable x
โit simply serves as a label to identify the variable to be doubled. Indeed, we could've replaced x
with any other label, as demonstrated in Approach 3 through the monkey emoji, ๐
.
By avoiding referencing any variable outside its scope, Approach 2 makes the function self-contained. This allows users to easily anticipate the consequence of executing double
through a simply inspection of the function, eliminating the need to review the entire codebase. Thus, Approach 2 aligns with the interpretation of a function as a self-contained mini-program: the function embodies the task of doubling a variable, turning the function reusable and applicable to any variable. In this context, applying double
to the global variable x
becomes just one possible application.
Structuring code around functions offers numerous advantages. However, to fully realize these benefits, users must adhere to certain principles when writing code. This section outlines a few of them and should be considered as a mere introduction to the subject. The topic will be investigated further, when we explore high performance.
The suggestion applies to both local variables and function arguments. Redefining these variables can have several disadvantages, including reduced code readability and potential performance degradation. Therefore, it's recommended that you define new variables instead of redefining existing ones. This approach is demonstrated in the following example.
function foo(x)
x = 2 + x # redefines the argument
y = 2 * x
y = x + y # redefines a local variable
end
function foo(x)
z = 2 + x # new variable
y = 2 * x
output = z + y # new variable
end
Within a function, Julia will throw an error if you perform a computation using a global variable x
and then redefine x
. For example:
x = 2
function foo()
y = x + 2
x = x + 4
return x
end
foo()
x
, causing Julia to consider x
a local variable. The consequence is that, when the function is run, x
in y = x + 2
is interpreted as a local variable that hasn't been defined yet.
We've emphasized the importance of viewing functions as self-contained mini-programs, designed to perform specific tasks. This perspective leads us to highlight the importance of modularity: the practice of breaking down a program into multiple small functions, each with its own distinct purpose, inputs, and outputs.
The primary benefit of modularity is the ability to work with independent code blocks. By keeping these blocks separate, we can decompose complex problems into multiple manageable tasks, making it easier to test and debug code. Additionally, modularity makes it possible to eventually improve or substitute parts of the code, without breaking the entire program.
A helpful way to understand this principle is by considering the analogy of building a Lego minifigure. In the first step, multiple blocks are created independently, each representing a specific part of the figure, such as the legs, torso, arms, and head. Then, in the second stage, these individual blocks are brought together and assembled into an integrated minifigure.
This two-step approach offers several advantages. By focusing on each block individually, we can concentrate and refine each part without worrying about the entire structure. Additionally, it provides great flexibility: since each block is created independently, we can modify specific blocks without having to rebuild the entire figure. For instance, if we want to change the figure's head, we can simply swap out the corresponding block, without starting from scratch.
The principle of modularity is closely tied to the suggestion of writing short functions. Some proponents even argue that functions should be limited to fewer than five lines of code Indeed, entire books have been written based on this principle. Although this viewpoint may be considered rather extreme, it clearly emphasizes the advantages of avoiding lengthy functions.
Coming up with mock scenarios illustrating the advantages of modularity can be tricky. This occurs because the same function could be deemed modular enough, depending on the context and your goals. Moreover, modularity may become detrimental if it impairs code readability. On top of this, note that making code more modular may not be justified if there aren't plans to reuse it.
With these challenges in mind, we'll present an example that showcases the potential benefits of modularity. The example focuses on calculating the cost of purchasing a product, when this is subject to a percentage tax over the total value. Specifically, consider the following two scripts to compute this.
expenditure(price, quantity, tax_rate) = price * quantity * (1 + tax_rate)
value_before_taxes(price, quantity) = price * quantity
valueAdded_tax(price, quantity, tax_rate) = price * quantity * tax_rate #it'll define the variable 'tax_paid'
expenditure(price, quantity, tax_paid) = value_before_taxes(price, quantity) + tax_paid
Consider now a scenario where the iPhone has a price of 1,000 USD and a tax rate of 5%. Then, we can apply these two mini-programs to compute the total expenditure of purchasing two iPhones.
#functions to compute expenditure
expenditure(price, quantity, tax_rate) = price * quantity * (1 + tax_rate)
#information
price = 1000
quantity = 2
tax_rate = 5 / 100
#computation
expenditure_iPhones = expenditure(price, quantity, tax_rate)
expenditure_iPhones
#functions to compute expenditure
value_before_taxes(price, quantity) = price * quantity
valueAdded_tax(price, quantity, tax_rate) = price * quantity * tax_rate
expenditure(gross_value, tax_paid) = gross_value + tax_paid
#information
price = 1000
quantity = 2
tax_rate = 5 / 100
#computation
gross_value = value_before_taxes(price, quantity)
tax_paid = valueAdded_tax(price, quantity, tax_rate)
expenditure_iPhones = expenditure(gross_value, tax_paid)
expenditure_iPhones
Approach 2 is more verbose, but also more readable, allowing the user to quickly grasp the code's purpose. In contrast, Approach 1 requires the reader to carefully examine each term of expenditure
to identify its functionality.
Furthermore, Approach 2 is more modular, as it breaks down total expenditure into two distinct components: the money to purchase the product without taxes and the taxes paid. While the benefits of such design may not be immediately apparent in this simple example, they'd be easily appreciated in a more complex scenario. There are several reasons for this.
First, Approach 2 offers greater flexibility compared to Approach 1. It can easily accommodate scenarios with multiple taxes or taxes of different forms, simply by recomputing tax_paid
. In contrast, fitting new cases through Approach 1 would necessitate modifying the entire script, including a full redefinition of expenditure
and its components. Second, Approach 2 is more convenient for testing code blocks separately. This feature is critical for ensuring proper code functioning, and can additionally simplify code debugging and the identification of performance bottlenecks.