Control Statements?
Table of Contents
Crucial to imperative programming is the concept of control flow within a function or method. To this end a number of branching statements are provided within Concurnas. These are: if elif else
, the if else
expression, while
, loop
, for
, parfor
, match
and with
. First, lets examine the blocks which these control statements are composed of.
Blocks?
Blocks form the foundation of control flow statements. They serve two purposes:
Defining scope - Variables and functions declared within a block cannot be used outside of that block. But code within a scope can use the functions and variables defined in parent nestor scopes.
Returning of values.
Simply put a block is a pair of curly braces:
{
//more code in here
}
Concurnas also offers a compact, single line block format =>
which can be used as follows:
def myMath(a int, b int, c int) => a*b+c
def mypy(a int, b int) => res = a**2 + b**2; return res**.5
Blocks are able (but not obliged) to return values if their final logical line of code returns something:
result = {
a = 2+3;
a**2
}//result is assigned value 25
Blocks which are used as part of control statements (soon to be elaborated) may return values as well, this becomes a very useful programming construct for creating concise units of code. For example:
result = if(cond1){
"condition 1 met"
}elif(cond2){
"condition 2 met"
}else{
"no conditions met"
}
In the case where there are two or more different types returned by the individual blocks associated with a control statement, the most specific type which can satisfy all the available returned types is chosen as the overall return type of the statement:
something Number = if(xyz()){
82//this is boxed to an Integer, which is a subtype of the more general type Number
}else{
new Float(0.2)//Float is a subtype of Number
}
All branches must return something for the above sort of code to be valid, if at least one branch does not return something, then this approach cannot be used (unless another flow of control statement causing breakout is encountered such as return
, throw
etc as the final line of the offending branch).
Since blocks return values, and lambdas/functions/methods are composed of a name (except for lambdas), signature and block, then we can skip having to use the return
statement in our function/method definitions.
So instead of:
def plusTwo(an int){
return an + 2
}
We can write:
def plusTwo(an int){
an + 2
}
In fact, with Concurnas, we can take this a step further by using the compact block form of =>
:
def plusTwo(an int) => an + 2
Note that the return type in the plusTwo
function definitions is inferred as int
. Sometimes however this auto-return behaviour is undesirable. Say we are trying to implement a function returning void and our last expression can return something, here we can use nop ;;
to suppress this being returned:
count = 0
def incrementor() => count++;;
Alternatively, we can specify the type for our function:
count = 0
def incrementor() void => count++
Anonymous Blocks?
It's often useful to specify a block on its own, without an attached control statement or association to a lambda/function/method etc. This provides one a bounded scope which is handy if one is working on a large/complex section of code and wishes to make clear a certain part of code is 'separate' functionality wise from the rest, and enable variable names to be potentially reused. It also allows one to return a value. For example:
//complex code here
oddcalc = {
a = 23
a*2 + a
}
anotherone = {
a = 57
b = 99
a*b-1
}
In fact, this turns out to be especially useful functionality in so far as defining isolates is concerned. For it enables us to write this kind of code:
res = {
//complex code here
a= 99
a + 1
}!//Use the ! operator to create an isolate
If elif else?
If, else, if else is a branching control statement. It is composed of a if test and block, optionally any number of elif (or else if) test and blocks and may optionally include an else block for when all the if test and any defined elif tests resolve to false. For example:
def fullif(an int) String {
if(an == 1){
"one"
} elif (an==2){
"two"
} else if(an ==3){//'elif' and 'else if' are considered to be syntactically identical
"three"
} else{
"other"
}
}
def ifelse(an int) String {
if(an == 1){
"one"
} else{
"other"
}
}
def ifelif(an int) String {
res = "unknown"
if(an == 1){
res = "one"
} elif (an==2){
res = "two"
}
}
def justif(an int) String {
res = "unknown"
if(an == 1){
res = "one"
}
}
In the final two examples above ifelif
and justif
, since no else block is provided, there is no certainty that all paths will resolve to return a value (it's possible that all the if and elif tests will fail) - so we cannot return a value from the if statements.
If else expression?
The if else expression, whilst not a statement, is translated into a if elif else statement (without any elif units) and behaves otherwise identically to an if-elif-else statement. An if test and else case must be included. For example:
avar = 12 if xyz() else 13
Functionally, this is identical to writing:
avar = if(xyz()){ 12 } else{ 13 }
Single expression test?
In Concurnas, a expression may be referenced in isolation within a test requiring a boolean value, e.g. an if
, elif
or while
test expression:
def gcd(x int, y int){
while(y){//translated to y <> 0
x, y = y, x mod y
}
x
}
In cases such as these where the value resulting from evaluating the test expression is non boolean
and in fact integral in nature, this is compared against the value 0
, if the result is non zero the expression resolves to true.
toBoolean?
As a corollary and in aid of our Single expression test above, all objects in Concurnas have a toBoolean
method automatically defined. This returns a single boolean
value resolving to true
. This value method may be overridden, for instance if one were defining a data structure, one may wish to define the toBoolean
as returning false
if the structure were empty.
The toBoolean
method is called when a single object is referenced where a boolean value is expected. Additionally, if the object is nullable, then it is checked for nullability, only if these two conditions are met is a value of true
returned. For example:
class MyCounter(cnt = 0){
def inc(){
cnt++
}
override toBoolean(){
cnt > 0
}
}
counter = MyCounter()
result = if(counter){//equivilent to: counter <> null and counter.toBoolean()
'counter is greater than one'
}else{
'counter is zero'
}
The behaviour for Strings, arrays, lists, sets and maps in Concurnas is slightly different from the above. In order to return true for:
Strings. The value must be non-null and non zero (i.e. not an empty String)
Arrays. The value must be non-null and the
length
field must be greater than 0.Lists, sets and maps. For
java.lang.List
,java.lang.Set
,java.lang.Map
's the value must be non-null and a call to theisEmpty
method must returnfalse
.
Loop?
The loop
statement is our first repeating control statement. It will execute the block of code attached to it repeatedly until either the code breaks out from the loop (by using the return
, break
or by throwing an exception) or the program is terminated - which is the less common use case. For example:
count=0
loop{
System out println ++count
if(count == 100){
break
}
}
While?
The while
statement is our second repeating control statement. It behaves in a similar way to loop but has a test at the start of the loop which if passed results in the attached block being called, at the end of the block the test is attempted again and if it passes then the block is called again, etc. If the test ever fails then repetition ceases. For example:
count = 0
while(++count <= 100){//test
System out println count
}//block to execute
For?
The for
loop statement is our third and final repeating control statement. There are two variants of for loop: c style for and iterator style for.
C style for?
C style for is the syntactically older of the two variants of for loop. In addition to its main block to repeat, it is composed of three key components:
An initialization expression, this is executed once at the start of the for loop. If a variable is created here it is bounded to the scope of the main block.
A termination expression. For as long as this resolves to
true
the loop is repeated, it is executed at the start of each potential repetition of the loop.A post expression. This is executed at the end of the loop.
Here is a typical instance of a c style for loop:
for(an = 0; an < 10; an++){
System out println an
}
Note that it's perfectly acceptable to omit any or all of the key components above, the following is equivalent to our loop example explored previously:
count=0
for(;;){
System out println ++count
if(count == 100){
break
}
}
Iterator style for?
The iterator style for is great for performing an operation per instance of an item in a list, array or otherwise iterable object - which is often the case about 80% of the time in modern programming with for loops. Instead of the three separate key components as par the c style for loop above, we just have one, an iterator expression using the in
keyword, which creates a new variable with scope bound to the main block of the for loop:
for(x in [1 2 3 4 5 6 7 8 9 10]){
System out println x
}
for(x int in [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]){//here we choose to specify the type of our new variable x
System out println x
}
In the above instances we are choosing to iterate over an n dimensional array and a list respectfully. We can also iterate over any object that implements the java.util.Iterator
interface, for example a range object:
for(x in 1 to 10){
System out println x
}
Parallel for?
parfor
looks a lot like a repeating control statement and indeed it does largely behave like a for loop in so far as iteration is concerned - indeed, both styles of for iteration are permitted, c style and iterator style. But execution of the main block is quite different. parfor
will spawn the maximum number of isolates it sensibly can (i.e. no more than one per processor core), and will assign each of the iterations of the for loop to these isolates in a round robin fashion, equally distributing the workload. Let's look at an example:
result1 = parfor(x=1; x <= 100; x++){
1 + x*10
}
result2 = parfor(x in 1 to 100){
1 + x*10
}
Bear in mind that code expressed in the main block that attempts to interact with the iteration operation (i.e. the value of x
in the first parfor
statement above) will not behave in the same way as an ordinary for loop. Likewise, break and continue are not permitted within a parfor
loop because the execution of the main block across a number of differing isolates occurs on a concurrent and non deterministic basis.
Instead of returning a java.util.List<X>
object, as par a regular for loop, an array of ref's corresponding to each 'iteration' of the main block is returned of type: X:[]
- where X
is the type returned from the main block.
Parforsync?
parforsync
behaves in the same way as parfor
, but all the ref's returned from the 'iterations' of the main block must have a value set on them before execution may continue past the statement - i.e. all iterations must have completed. Example:
result1 = parforsync(x=1; x <= 100; x++){
1 + x*10
}
result2 = parforsync(x in 1 to 100){
1 + x*10
}
Repeating Control Statements Extra Features?
The Repeating control statements; loop
, while
, for
, parfor
and parforsync
share some common extra features which make them especially useful.
else block?
An else
block can be attached to all the repeating control statements, except loop
, parfor
and parforsync
, and allows one to cater for cases where the attached main block is never entered into (i.e. the relevant test resolves to false on the first attempt):
res = ""
while(xyz()){
res += "x"
}else{
res = "fail"
}
res = ""
for(x in xyz()){
res += x
}else{
res = "fail"
}
Returns from repeating control statements?
The repeating control statements may return a list of type java.util.List<X>
where X
is the type returned by the attached loop block - int
in the below example:
count=0
series = loop{
if(count == 100){
break
}
++count//returns a value of type int
}
For series
above the type is java.util.List<int>
when used in a for loop:
series = for(a in 1 to 100){ a }
Note that parfor
and parforsync
have their own, different, return logic covered previously.
The index variable?
All the repeating control statements (except the c style for loop) may have an attached index variable. This is handy in cases where one say wishes to use the Iterator style for loop, but also have a counter for the number of iterations so far. The index may have any name and by default the type of the index is int
. long
is a popular choice if you need to explicitly define the type of the index where working with very large iterations. An initial value assignment expression may be defined, if one is not then the value of the index is set to 0
. For example, with a for loop:
items = [2 3 4 5 2 1 3 4 2 2 1]
res1 = for(x in items; idx) { "{x, idx}" }//idx implicitly set to 0
res2 = for(x in items; idx long) { "{x, idx}" }//idx implicitly set to 0L
res3 = for(x in items; idx = 100) { "{x, idx}" }
res4 = for(x in items; idx long = 100) { "{x, idx}" }
alreadyIdx = 10//index tobe used defined outside of loop
res5 = for(x in items; alreadyIdx) { "{x, alreadyIdx}" }//alreadyIdx already defined
Index variables may be applied to loops and while loops in a similar fashion to iterator style for loops:
items = [2 3 4 5 2 1 3 4 2 2 1]
n=0; res1 = while(n++ < 10; idx) { "{n, idx}" }//idx implicitly set to 0
n=0; res2 = loop(idx) {if(n++ > 10){break} "{n, idx}" }//idx implicitly set to 0
With statement?
The with statement which, though not being an actual control statement, does look a lot like one, and for that reason is included here. This is particularly useful in cases where one must call many methods, or interact with many fields of an object in a block of code. Using the with statement allows us to skip having to explicitly reference the object variable of interest:
class Details(~age int, ~name String){
def getSummary() => "{name}: {age}"
def rollage() => age++;;
}
det = new Details(23, "dave")
with(det){
name = "david"
rollage()
}
det.getSummary()
/
=> "david: 24"
With statements, as with all block based code in Concurnas can return values:
class Details(~age int, ~name String){
def getSummary() => "{name}: {age}"
def rollage() => age++;;
}
det = new Details(23, "dave")
summary = with(det){
name = "david"
rollage()
getSummary()
}
//summary == "david: 24"
With statement may be nested. In instances where there is an identically callable method in the nesting layers, then the innermost layer is prioritized.
Break and Continue?
The break
and continue
keywords may be used within all the repeating control statements: loop, while, for (but not parfor
or parforsync
) in order to affect the flow of control within the main block of the statements.
The break
keyword allows us to break out (escape from) from the inner most repeating control statement main block, and continue on with execution of the code post the control statement.
The continue
keyword allows us to terminate the current repetition of the inner most repeating statement main block and carry on execution as if we had reached the end of the main block attached to the statement normally - so for a loop
this would be the start of the main block, for a while
it would be the beginning of the test etc.
Let's look at an example of continue
and break
:
items = x for x in 1 to 50
for(x in items){
if(x <== 5){
continue //go back to the start of the loop, i.e. skip the code which outputs to console
}elif(x == 9){
break//go to the code after the for loop
}
System out println x
}
The continue
and break
keywords may be used when the repeating control structures which return a value for each iteration in the following manner:
series = for(x in items){
if(x <== 5){
continue x//continue but add a value to the result list
}elif(x == 6){
continue//continue and don't add a value to the result list
}elif(x == 9){
break x//break but add a final value to the result list
}elif(x == 10){
break//break and don't bother to return a value to the result list
}
x//if we've made it throught the above, then this value is added to the result list
}