Variables
There is more than one way in Jule to define a variable.
Jule is uses the let
and const
keywords for variable declarations. Jule uses the :
operator for type annotations. When you don't use :
operator, you report you want the type to be detect automatically by compiler. The type is set by the compiler based on the data. Aka type inference.
For example:
let age = 18
The data type of the above variable is defaulted to int
.
Type Annotation
You may want to annotate the type, it is possible. It also brings you advantages.
For example:
let age: int = 18
This is equivalent to the one shown above. However, you annotated the type. So what exactly is the difference? We mentioned that it has advantages, let's take a look:
- When you follow this method, you do not have to give a value during the creation of the variable. According to the data type you have defined, the compiler initializes that variable with the default value of the given data type if you have not given a value.
- This is better than automatic assignment if the variable's data type is important. Because in automatic assignment, the data type is detected automatically, but since the value given here must be compatible with the given data type, the Jule compiler will not accept any incompatible value.
- You know more precisely which data type you are working with. It can help avoid possible confusion.
Auto Initialization
In case of type annotation, the default value is initialized by the compiler. Variables do not need to be explicitly initialized when the type is annotated.
For example:
let a: int
Assignment
The values of the variables can be changed later. The value given must be the same as the data type of the variable.
For example:
fn example() {
let mut a: int = 10 // Value is 10.
a = 200 // New value is 200.
}
TIP
Variables must be mutable in order for their values to change. For this, the keyword 'mut' was used in the example above. For more information about mutability, you can refer to the mutability section.
Static Variables
Static variables are initialized once, they are living allocations over the life of the program. They are declared with the static
keyword and, like constant variables, an initialize expression is required.
For example:
static myInt: int = 20
INFO
Binded variables can't be static.
Static Local Scope Variables
Using static in fields where the concept of local scope is valid, such as function body, has the same effect as global scope static variables. Your static variable is allocated once and survives for the lifetime of the program. Therefore, the value of the variable can likewise be preserved between calls because the variable is a statically stored.
For example:
fn count() {
static mut a: int = 0
a++
println(a)
}
fn main() {
count()
count()
count()
count()
count()
count()
}
In the example above, the variable a
is static so it will retain its value and you will get an integer that is constantly increasing.
Global Variables
Global variables are in the main scope of the program and are static variables by nature. Therefore, variables in the global scope must be declared as static.
For example:
static myInt: int = 20
fn main() {
// ...
}
INFO
Binded globals can be declare via let
keyword.
Constant Variables
See relevant comptime documentations for information about constant variables.
Shadowing
In the basic sense, shadowing is when a definition with the same identifier shadows a define with the same identifier before it in scope. This is made possible by performing a new definition in subscopes of a scope with the name of a definition defined in that parent scope, or by using the identifier of a global definition in the main scope of a function.
Shadowing can cause various developer errors and make reading code more complicated. Therefore, by default your compiler does not allow shadowing. However, you can enable this by passing the --shadowing
option to your compiler.
What if Shadowing Enabled
When you tell your compiler to allow shadowing, it allows you to implement shadowing. What Shadowing is explained. To give an example:
fn main() {
let a = 10
{
let a = 200
println(a)
}
println(a)
}
In the above example, the main scope of the function has a child scope. This scope has a variable with the same identifier as the variable a
in the main scope. This variable replaces and shadows the parent scope's variable a
in it and its child scopes.
Can a definition in the same scope be shadowed, how does the compiler behave about it? Obviously, you can't. The compiler will never allow two identifiers in the same scope. Therefore, you cannot have definitions with the same identifier in the same scope.
Multiple Assignment / Declaration
You can multiple variable assignment or declaration. What? Sure, you can use two type in same statement. You know how to declare variable, okay it is same. The single difference, identifiers and expressions separate with comma.
For example:
let (x, y, z) = true, 1, -400
Yes, there is we declare three new variable named as x
, y
and z
with auto-type detection. Variable values are; x
is true
, y
is 1
and y
is -400
. As you can see, the order in which the variable is defined is associated with the expression in the same order. Remember that, you can't use type annotation in this case.
Use the mut
keyword for mutable declaration.
For example:
let (mut x, y, mut z) = true, 1, -400
The x
and z
variables are mutable.
So how do we do the assignment thing?
We're essentially just removing the declaration things that indicates that the variable is a new variable. Actually, only the name of the variable needs to remain, remember value assignment statement.
For example:
x, y, z = true, 1, -400
The logic remains the same. Let's not forget that these variables must already exist, of course, due to static type principles, the value you show to it must be compatible with the data type of the variable.
Ignore Identifier
Additionally, you can skip some values with ignore identifier: _
. For example:
x, _, z = true, 1, -400
let (x, _, z) = true, 1, -400
This way you ignore some values.
Additionally, if you have a tuple expression and want to ignore all expressions with ignore identifier, you do not need to write ignore identifier for each expression individually. You can ignore the entire tuple expression by using a single ignore identifier.
For example:
let (a, b, c) = 10, 20, false
_ = a, b, c
Assignment and Definition Simultaneously
Script: You have multiple assignments, but some of your variables need to be defined for the first time while some of your variables are assigned.
Jule's approach in this regard is that both can occur simultaneously if certain rules are met. If you are using a variable defined in the same scope, the variable will not be evaluated according to the definition rules. This will not cause you to get an error about it. Your variable is handled according to the assignment rules. So the errors we can get will be typical assignment errors like mutability or type safety.
For example:
fn main() {
let (mut x, y) = 10, 20
let (x, z) = 100, 30
println(x) // 100
println(y) // 20
println(z) // 30
}
In the example above, the variable x
is set to mutable. Since it is again in a multiple assignment within the same scope, it is treated according to the assignment rules, not the rebuild rules. While assigning to the x
variable, the z
variable does not exist, so it is newly created.
The point that should not be missed is that the relevant variable must be in exactly the same scope. If a variable from the global scope is used, it will not be considered as assignment. Likewise, if the variable comes from parent scopes, it is still not considered an assignment.
For example:
fn main() {
let (x, y) = 100, 200
{
let (x, z) = 10, 20
println(x) // 10
println(y) // 200
println(z) // 20
}
println(x) // 100
}
In the above example, the variable x
is not considered an assignment because it comes from the parent scope. If you want to perform an assignment, you must use a clean assignment statement. The above example cannot be done by default, this requires allowing shadowing.
Also if you want to perform an assignment, but not to a variable but to a pointer etc. You can do this.
For example:
fn main() {
let mut a = [1, 2, 3, 4]
let (a[0], b) = 20, 30
println(a) // [20 2 3 4]
println(b) // 30
}
WARNING
This can make the statement very complex and impair readability. For this reason, it is recommended not to use too complex expressions.
Short Declarations
Short declaration is a language feature that allows you to declare variables with a shorter and simpler syntax without using the let
keyword.
The syntax is almost the same as the let
keyword and the same rules apply.
The :=
operator is used to define a short declaration statement. This operator means that it is a declarative assignment expression, not an assignment. That is, it is a semantic shortcut equivalent to let (...) =
and and are handled in the same way.
For example:
fn letKeyword() {
let mut a = 0
let b = false
let &c = a
let (x, mut y, &z, _, mut &k) = "foo", "bar", b, false, a
_ = x, y, z, c, k
}
fn shortcut() {
mut a := 0
b := false
&c := b
x, mut y, &z, _, mut &k := "foo", "bar", b, false, a
_ = x, y, z, c, k
}
INFO
You can use whichever method you find more useful/readable. Jule does not make any recommendations or set standards on this matter.