Variable Assignment?
Table of Contents
We don't need to be related to relate
We don't need to share genes or a surname
You are, you are
My chosen, chosen family
Let us first define what is means to declare a variable. A variable declaration consists of a variable name and a type. Variable names can be any combination of standard ASCII alphanumeric characters or non ASCII Unicode characters. Types may be either primitive, tuples, typedefs, Object types (which includes user defined classes) or refs to the aforementioned types. For example:
anint int //a primative type
atuple (String, int) //a tuple
typedef MyMap = java.lang.HashMap<String, String> //a typedef
aMyMap MyMap //use of a typedef
class MyClass //a class declaration
anObject MyClass //use of a class
aref int: //a ref of type int
Scopes?
Usually if we declare a variable then we'd wish to assign a value to it at some point during the bounded scope of said variable. The bounded scope constitutes the inner most pair of curly braces and any nested braces within in which the variable is declared. Variables can only be assigned to and used within their bounded scope.
if(something()){
anint = 99
if(somethingelse()){
anint = anint + 1
}
}
//anint cannot be used after this point as that usage would be outside its bounded scope, the following is invalid:
res = anint//invalid useage
It can sometimes be useful to define a scope on its own (i.e. not associated with a control structure such as a if else statement or a function definition etc). This can be achieved as follows:
{
anint = 99
//do somethiing with anint
}
//anint is no longer in scope at this point...
Assigning values?
We can assign a value to a predeclared variable using the assignment operator =
as follows:
avar int//declaration
avar = 99 //assignment
More commonly we perform the initial assignment and variable declaration in the same step. This has the added advantage of allowing Concurnas to use type inference (if we wish) in order to determine the type of the variable for us, saving us the effort of having to write out a verbose type and increasing readability of our code.
So instead of this:
avar int = 99
We can write this:
avar = 99
Concurnas is an optionally type inferred language. What this means in practice is that 90% of the time it's not necessary to explicitly specify the type of a variable
The optionality in Concurnas' design regarding inference is useful for describing problems with complex typing structure which otherwise would not be obvious to determine from reading the code.
val and var?
The val
and var
keywords are useful in two ways. First, either one make it explicitly clear that one is defining a new variable and secondly, the val
keyword stipulates that no assignment post initial declaration/assignment is permitted. For example:
var anint = 99
val another = 99
Both the above variables are new (it would be a compilation error if they had already been declared in the scope prior to the above code), and in the case of the another
variable, it cannot be reassigned.
Unassigned declared variables?
It's considered a compilation error to define a variable but never assign a value to it (or to attempt to use a variable at a point after declaration when it's possible that execution of the code has progressed through a path in which a value is not assigned). The only case in which it's valid to declare a variable but not assign it a value is where the variable is a of a ref type (used to communicate between isolates - the principle unit of concurrency in Concurnas). For example:
def assignIntRef(an int:){//function to assign a value to a ref
an = 15
}
aint int: //declaration of a local ref
onchange(aint) => System out println "aint was assigned: {aint}"
assignIntRef(aint)
Multi Assignment?
Sometimes it can be useful to assign a value to more than one variables. To this end Concurnas supports multi assignment:
a = b = 20
//a == 20
//b == 20
We can have as many expressions as we like on the left hand side of the multi assignment before the final expression which we are assigning from. The expressions on the left hand side chain can be of any format normally valid on the left hand side of an expression:
ar = new int[10](0)
val aval = ar[1] = ar[5] = 30
//aval == 30
//ar == [0 30 0 0 0 30 0 0 0 0]
Note that the expression on the right hand side is only evaluated once, the assignees on the left hand side chain receive the same reference to the right hand side value, i.e. No copy is made.
myArray = [1 2 3]
b = c = myArray
b[0] = 99
//b == [99 2 3]
//c == [99 2 3]
One is not limited to the use of just the =
assignment, one may use any valid assignor in the chain:
a = 100
a += b = 2
//a == 102
//b == 2
Compound assignment?
Compound assignment refers to assignments performed on variables which have been defined already and which apply an operator in addition to assigning a new value to said variable. These are: +=
, -=
, *=
, /=
, **=
, mod=
, or=
, and=
, <<=
, >>=
, >>>=
, band=
, bor=
, bxor=
. Taking the addition assignment operator, +=
as an example, the following two statements are functionally identical:
a1 = a2 = 10
a1 += 1
a2 = a2 + 1
assert a1 == a2
That is to say, a1 += 1
is essentially shorthand for: a1 = a1 + 1
.
The compound assignment operators may be applied to array reference operations as follows:
a1 = [1 2 3]
a1[0] += 1
assert a1 == [2 2 3]
Again, a1[0] += 1
is essentially shorthand for: a1[0] = a1[0] + 1
.
Lazy variables?
The form of evaluation used in Concurnas is "eager evaluation" - i.e. as soon as an expression is bound to a variable, it is evaluated. Additionally, this can occur concurrently in the case of ref assignments occurring in differing iso's. Let's look at an ordinary example:
myvar = {1+3}
In the above example, after the 1+3
expression is bound to the variable myvar, it is executed. Thereafter myvar holds the value of 4.
There are some problems for which this form of eager evaluation is inappropriate and for which we'd prefer to defer execution of the expression until the associated bound variable is accessed. This is lazy evaluation and Concurnas offers a controlled form of this expressed in the form of lazy variables.
We can define a lazy variable as follows:
lazy myvar = {1+3}
Now, only when myvar is accessed is the bound expression 1+3
evaluated. Note also that this evaluation occurs only once. Subsequent assignments to the variable are possible and the associated bound expressions will only be evaluated once the variable is accessed, just like the initial assignment.
Let's look at a more interesting example. First, normal execution without a lazy variable:
a = 100
myvar = {a=400; 22}//assign, with side effect of re-assigning a
res = [a myvar a] //lets create an array holding the value of a, the evaluated myvar and a again
//res == [400 22 400]
In the above case we see that in assigning a value to myvar
, we invoke the side effect of assigning a new value to a. Since this occurs as myvar
is initially created, before we have a chance to first access a
, we only see a as having the new value of 400
. And this is reflected in the first and last elements of the res
array referring to the value of variable a
. Let's change this with a lazy variable:
a = 100
lazy myvar = {a=400; 22}//assign, with side effect of re-assigning a
res = [a myvar a] //lets create an array holding the value of a, the evaluated myvar and a again
//res == [100 22 400]
Now we can see that we can see the initial value of a, since when we are creating our res
array, we access a before we access myvar
, since the expression bound to myvar
is lazy evaluated and has not yet been evaluated, the side effect assigning the value of 400
to a has not yet occurred. However, after accessing myvar
for our second array value, a is not assigned the value 400
and this is reflected when we extract the value for the final element of the array.
Where function or method parameters are defined as being lazy, the passed bindings are only evaluated upon use within the function or method. For example:
a = 100
def makeArray(lazy operate int) => [a operate a]
res = makeArray({a=400; 22})
//res == [100 22 400]
We see in the above example, that the block passed as an argument to the makeArray
function is only evaluated during execution of said function - and not initial invocation of the function.
Behind the scenes, in order to implement lazy variables, we are overloading the unassign operator(see operator overloading). What if we don't want to evaluate the bound expression of a lazy variable when we refer to it, for instance when passing it to a function invocation? In this case we can use the :
operator as follows:
a = 100
lazy myvar = {a=400; 22}//assign, with side effect of re-assigning a
def makeArray(lazy operate int) => [a operate a]
res = makeArray(myvar:)
//res == [100 22 400]
Here we are preventing unassignment of myvar
, instead this (and therefore execution of the binding of myvar
) is allowed to occur within the makeArray
function.
We can make use of lazy variables within function/method references as follows:
a = 88
def afunc(lazy operate int) => [a operate a]
xx = afunc&(lazy int)
res = xx({a=400; 22})
//res == [100 22 400]