Types?
Table of Contents
In a sense, all programming is about data. As such the way in which we represent data, via types, is a fundamental concept in any programming language. In Concurnas, roughly speaking we have two foundation types: primitive and object types. From which we are able to derive six composite types of: arrays, method references, generics, actors, references and tuples.
Why does Concurnas support both primitive types and Object types (after all, primitive types can all be represented via object types). Two reasons: performance and memory utilization. Generally speaking if we represent data which can be expressed as a primitive type using an object, then we consume more memory than we need to and can make the job of memory management (garbage collection, heap vs stack allocation etc) more difficult than it needs to be. Additionally our CPU processors are designed to perform operations on primitive type data, so by using objects we are adding a level of indirection which of course has a (relatively minor) performance penalty. In any case, although behind the scenes via a compiler/runtime optimization this penalty is mostly eliminated, it is still useful to be able to give the programmer control over the use of primitive or object types.
That being said there are some circumstances in which using primitive types may seem to be a good idea, but we are forced instead to implicitly use object types - for instance, with generics. This is generally done to make code and libraries easier to understand, utilize and to eliminate code duplication.
Primitive Types?
Let us first begin with the numerical primitive types of which there are eight:
Type Name |
Bit width(bytes) |
Min/max values |
---|---|---|
|
32 (1) |
|
|
16 (2) |
\({-2^{16}, 2^{16} - 1}\) |
|
8 (1) |
\({-2^7, 2^7 - 1}\) |
|
16 (2) |
\({-2^{15}, 2^{15} - 1}\) |
|
32 (4) |
\({-2^{31}, 2^{31} - 1}\) |
|
64 (8) |
\({-2^{63}, 2^{63} - 1}\) |
|
32 (4) |
|
|
64 (8) |
\(^{a}\)internally to Concurnas (or to be more precise, the JVM) a 32-bit int is used to represent a boolean.
\(^{b}\)int is the default type used when an integer literal is expressed.
\(^{c}\)double is the default type used when a decimal number literal is expressed.
The byte, short, int and long types allows us to store integers (whole number) values accurately. If one wishes to store real numbers (i.e. with a decimal point), then the float and double types are available (with double offering more precision) - though they can only offer a finite degree of precision post the decimal point. For an accurate decimal representation to a fixed degree, using a fixed point decimal representation such as the object type: java.math.BigDecimal is recommended.
A int literal can be expressed as:
Decimal:
123
Binary:
0b010110
Hexadecimal:
0x0E
A long literal can be expressed as:
Decimal:
123l
or123L
A short literal can be expressed as:
Decimal:
16s
or16S
A byte literal must be cast from:
An int of value between \({-2^7, 2^7 - 1}\) using the cast operator
as
. e.g.:12 as byte
A float literal can be expressed as:
Decimal:
12f
or12F
Real:
12.2f
or12.2F
Engineering:
12e6f
or12e6F
A double literal can be expressed as:
Decimal:
12.
or12d
or12D
Real:
12.2
or12.2d
or12.2D
Engineering:
12e6
or12e6d
or12e6D
Char's?
Characters are represented by the primitive type char
, are 32 bits wide and can be expressed as a single character surrounded by either a pair of ''
or ""
:
achar = 'a'
achar = "a"
Characters are of some use in modern programming, but Unicode Strings (as seen later) are usually more useful.
Booleans?
Booleans are represented by the primitive type boolean
or bool
(use of which is a matter of personal preference), and are expressible as: true
or false
:
abool = true
another boolean = false
Bytes?
Bytes are represented by the primitive type byte
, and are creatable via a cast:
mybyte = 0x011010 as byte
Choosing primitives?
When choosing which primitive type to represent our numerical data with, it is important to consider the range of values that our data can have and choose the smallest bit width type which best aligns to that data (so as to conserve memory). This is especially the case when defining large n dimensional arrays of data. However, these days our systems are generally non-memory bound, and as such over allocating and say using an int
where a short
would be more appropriate is not considered a problem. If in doubt over allocate, and don't worry too much.
Strings?
It turns out that in modern programming Strings are so proliferant that they deserve their own first-class citizen support (believe it or not, but there was a time when this was not the case!). In Concurnas there are a few handy special considerations to the language just to aid in working with Strings. Note that strings themselves are objects (covered in the next section).
At the most basic level, Strings can be defined via use of either ''
or ""
. For example:
astring = 'Hello World!'
another = "I'm also a String"
Note above that by permitting both ""
and ''
for string defining, we're able to use '
and "
respectfully in our Strings. But, if one does not wish to switch between one style or another in order to use these characters inside a string, or has both a '
and a "
in the string one is defining, then an escape character \ may be used. For example:
astring = 'I\'m also a String'
So as to disambiguate between the definition of a String and a character, for cases of single character length strings, using the ''
denotes a character, and ""
denotes a String. For example:
aString = "c"
aChar = 'c'
The escape character?
As we have already seen the escape character \ can be used to input a '
or "
inside a ''
or ""
string. It may also be used to input a range of special characters including the newline character: \n and Unicode, UTF-16 characters. For example:
anewline = "Hello\nworld!"
/*=>Hello
world!*/
aString = "\u0048\u0065\u006c\u006c\u006f \u0077\u006f\u0072\u006c\u0064\u0021" //"=>Hello world!"
The String concatenation operator +?
We can compose strings by using the concatenation +
operator. This is useful for mixing raw text or pre-existing string variables and variables/expression values together. For instance:
avar = 8
str = " there m"
aString = "hi" + str + avar
//aString =="hi there m8"
In order for string concatenation via the +
operator to work, at least one of the right-hand side or left-hand side arguments must be a String, however, even an empty String ""
is acceptable.
In the case where an object type is involved in String concatenation the object's toString
method is invoked. This method is included for all objects by virtue of the fact that they all inherit from Class java.lang.Object
where a default implementation returning the objects class name and hexadecimalized hashCode is returned as a string.
In the case where an n dimensional array is on the left or right hand side of the concatenation it will be converted into a formatted String. For example:
mat = [ 1 2 ; 3 4]
asStr = "" + mat
//asStr == "[[1 2] [3 4]]"
Note that since strings are immutable objects, a new String is created as a result of the +
operator being applied.
Assignment plus +=?
The assignment plus operator +=
operates in a similar manner. If the item on the left-hand side of the plus assignment operator is a String, then the item on the right-hand side of the plus assignment operator is appended to the left-hand side and a new string is assigned to the left-hand side item.
String is an Object?
The type String is an object - therefore it can be created in the usual way for Objects:
str = new java.lang.String(23)
Code in Strings?
Another handy trick when creating strings is to nest raw code within the string itself. This is great for including variables directly in strings without having to resort to the somewhat cumbersome usage of multiple +
operators as par above.
avar = 8
str = " there m"
aString = "hi {str} {avar}"
//aString == "hi there m8"
We can include more complex expressions as follows:
aString = "addition result: {1+2}"
//aString == "addition result: 3"
Code in Strings can be applied everywhere except for when defining Strings for default annotation parameters - as these are required to be fully-defined constants at compilation time. Code within strings nest, that is to say that; code embedded within the string may nest other strings which may optionally have code within them too.
Escaping code in Strings?
Code in Strings may be escaped by prefixing said code with a \:
aString = "addition result: \{1+2}"
//aString == "addition result: {1+2}"
Format Strings?
One final mechanism by which we can create Strings is via the format, static method of java.lang.String
var1 = 99
var2 = 234.
String.format("var1: %s var2: %s", var1, var2)
Additional operations on Strings?
Concurnas supports the additional following set of basic operations on Strings:
Character at?
The character at a specific point in a String may be determined:
achar = "abcdefg"[2] //c
Iteration?
Iteration over all characters of a String:
elms = (x, x == 'b') for x in "abcdefg"
//elms ==> [(a, false), (b, true), (c, false), (d, false), (e, false), (f, false), (g, false)]
More information on Strings?
Strings in Concurnas are supported by the JVM, for more information on Strings see: Oracle Java String documentation.
Regex?
Regex, or regular expressions, are another area of modern programming so proliferant that Concurnas has special first-class citizen support. We can define a regex in much the same way as a String, by prepending the regex String within ''
or ""
with r
, to make r''
or r""
for example:
aregex = r"ab*a"
...
amatch = aregex.matcher("abbbba").matches()
This syntax produces an object of type java.util.regex.Pattern
. More details of this can be found here. For more details on the variant of regex supported by Concurnas, see here. Note that it's relatively expensive to construct regex objects since they are compiled at runtime upon definition so its recommended that they be created as top level variables or cached and/or otherwise reused.
Object Types?
Objects and Classes are covered in more detail in the classes section. This is a brief introduction:
open class MyClass(public an int)
class ChildClass extends MyClass(88){
def amethod() => "returns something"
}
myObject1 = MyClass(12)
myObject2 Object = MyClass(12)
myChildObj1 = ChildClass()
myChildObj2 MyClass = ChildClass()
myObject1
and myObject2
are instance objects of type class MyClass
. However, in the case of myObject2
since it has been declared as being of type Object
- it can only be used as an instance of Object, the an
field is not accessible unless the object is cast to an instance of MyClass
.
myObject1
and myObject2
are instance objects of type ChildClass
. However, in the case of myChildObj2
since it has been declared as being of type MyClass
- it can only be used as an instance of MyClass
, the amethod
method is not callable unless the variable is cast to an instance of ChildClass
. For myChildObj1
we can access the field an
from the superclass MyClass
.
Note that java.lang.Object
is an implicit superclass of every class if a super type is not defined. As such it is the superclass of every instance object.
null?
Null is a special type of Object and is represented simply by the keyword null
. Any object may be assigned a value of null
as it is a subtype of all Object types (including arrays). Attempting to call a method or access a field on a nullable object which is null
will result in a java.lang.NullPointerException
object being thrown. This means that where one suspects an object may be null, it is necessary to check for the null state. As such null should only be used sparingly.
Generally speaking, the conventional wisdom of modern software engineering is that null should be used sparingly. As such if one wishes for a variable to be potentially nullable, its type must be declared as being nullable, this is denoted by appending a ?
to the type. For example:
aString String = "hi"
nullable String? = null
nullable = "no longer null"
The subject of null safety is covered in detail in the null safety chapter. For now let's look at how we can use null
:
assignedNull String = null //an Object of type null assigned a String
aArray int[]? = null
aMatrix int[2]? = null
class User(name String, age int)
def extractName(user User?){//function taking an object argument which may resolve to null
"unknown" if user == null else user.name //testing for nullability
}
def extractAge(user User? = null){//function with a default parameter resolving to null
0 if user == null else user.age
}
name = extractName(null)//passing null as an argument to a function
age = extractAge()//the single argument to the function call will be populated with its default value of null
(Un)Boxed primitive types?
For every primitive type, there exists an Object type. Concurnas allows either to be used on an interchangeable basis via a process known as boxing and unboxing.
The pairs of primitive and object types are as follows:
Primitive Type |
Object Type |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
These can be used interchangeability for example:
Boxing?
def opOnInteger(an Integer) => an+1
opOnInteger(12)
Above, the value passed as an argument in the call to opOnInteger
is 'boxed' to type Integer. Behind the scenes the code is converted into the form:
opOnInteger(new Integer(12))
Unboxing?
def givesAnInteger(from int) => new Integer(from)
avar int = givesAnInteger(12)
Above, the value returned from givesAnInteger
, of type Integer
, is 'unboxed' to type int
. Behind the scenes the code is converted into the form: givesAnInteger(12).intValue()
Autoboxing and unboxing also conveniently allows us to use primitive types as generic type qualifiers. For example:
anAr = new java.util.Arraylist<int>()
Arrays?
Arrays are covered in more detail in the arrays section. This is a brief explanation.
When we refer to arrays we are typically referring to a one dimensional array. For example:
arra1 int[] = [1 2 3 4 5]
arra2 int[1] = [1 2 3 4 5]
The Array type is simply: normal type with []
postfixed or [1]
- indicating one level of dimensionality.
When it comes to n dimensional arrays, for instance a matrix (n=2), the type syntax is very similar:
mat1 int[][] = [1 2 ; 3 4]
mat2 int[2] = [1 2 ; 3 4]
Hence, the syntax to define an n dimensional array is: normal type, with n []
's or [n]
.
An array can be composed of any type - in other words, you can have an array of elements of any type in Concurnas. However, all the elements of said array need to be of the same type. For example the type of this array:
["hi", 23]
Is java.lang.Object
, since this is the most specific type which is a supertype of all elements of the array (the second int
item will be boxed to an Integer, which is a subtype of java.lang.Object
).
We can extract an array from a matrix and an individual item from a matrix as follows:
mat int[2] = [1 2 ; 3 4]
ar int[] = mat[1]
item int = mat[1, 2]
When we perform an m level extraction we are returning a type: composed[n-m], and where n-m == 0 the composed type itself is returned.
When we set values of n dimensional matrices, they do not have to match 1:1 with the declared type of the matrix, but they may be subtypes. For example:
objAr Object[] = [ "hi", "there", 123]
objAr[2] = "friend"//replace last item with a String
Method References?
Method (or function) references can be created via a number of mechanisms; In addition to being the types of method references they are the types of lambda definitions. They take the form of a list of input arguments (which may be empty) and a return type. For example:
def plusTogether(a int, b int) => a+b
ref (int, int) int = plusTogether&//create a function reference to plusTogether
aslambda (int, int) int = def(a int, b int) => a + b//create a lambda
They are handy types and can be passed around our program and invoked as follows:
ans1 int = ref(1, 1)
ans2 int = aslambda(1, 1)
All method references are a subtype of lambda
which is itself a shortcut for
com.concurnas.bootstrap.lang.Lambda
:
def doer(a int) => a*2
afuncref lambda = doer&
afuncref com.concurnas.bootstrap.lang.Lambda = doer&
An alternative representation for a method reference is to use their associated object type. For a method reference returning a non void type:
def doer(a int) => a*2
afuncref (int) int = doer&
altrep Function1<int, int> = afuncref
And for a method reference returning void:
def doer(a int) void {}
afuncref (int) void = doer&
altrep Function1v<int> = afuncref
Composite types of Method References?
A type which is composed of method references can be defined by surrounding the method reference in parentheses. This is handy for defining array or method references with arguments or return types themselves being method references:
def applyMethodRefs(what ((int) int)[]) //takes an array of (int) int method refernces as input
=> w(20) for w in what
duplicator ((int) int) ((int) int)[] = def ( input (int) int ) => [input input]
duplicatorDup (((int) int) ((int) int)[])[] = [duplicator& duplicator&]
Generic Types?
Generics are covered in more detail in the Generics chapter. This is a brief explanation.
Let us create an instance object of a class that supports Generics:
class MyGenClass<X>
instObj1 MyGenClass<String> = new MyGenClass<String>()
Above we create an object instObj1
which is of type MyGenClass
and having generic qualification String
since MyGenClass
requires one generic type qualification which it refers to internally as X
. When creating instance objects of classes requiring generic type qualification - these generic types must be provided.
Bear in mind that MyGenClass<String>
is not a subtype of MyGenClass<Object>
- the generic type qualifications must match, they cannot be subtypes.
In instances, except creation, where we really do not know the generic type, or don't care for it, then we can use the wildcard ?
in place of their declaration. For example:
instObj2 MyGenClass<?> = new MyGenClass<String>()
Tuples?
Tuples are covered in more detail in the tuples section. This is a brief explanation:
atuple = 12, "hi"
another (int, String) = 12, "hi"
another2 Tuple2<int, String> = 12, "hi"
All three above are equivalent and resolve to a tuple with two elements of Type: (Integer, String)
.
Typedefs?
Typedefs are covered in more detail in the Typedefs section. They are most commonly used in order to avoid code duplication in writing long type definitions (particularly those with many complex generic type qualifications) so as to improve code readability. Let's briefly create and use a typedef:
typedef MyMap = java.util.HashMap<String, java.Util.HashSet<int>>
am MyMap = new MyMap()
am2 java.util.HashMap<String, java.Util.HashSet<int>> = new java.util.HashMap<String, java.Util.HashSet<int>>();
am
and am2
are essentially equivalent to one another in type. At compilation time the first definition and assignment to am
is expanded into the second form of am2
.
At this point the key thing to note with typedefs is that they are essentially shortcuts ('drop in replacements') for otherwise longer, more verbose type definitions.
Refs?
Refs are covered in more detail in the Refs section. For now let's look at a brief introduction to refs from a typing perspective:
aref1 int: //defined but unassigned local ref of type int
aref2 int := 21 //defined and assigned local ref of type int
aref3 := 21 //assigned local ref of inferred type int
aref4 = 21: //assigned variable of inferred type int: with the right-hand side being a ref creation expression
aref5 = {21}! //assigned variable of inferred type int: - right-hand side being a ref returned from an isolate - this is the most computationally expensive variety of ref creation - if you find yourself doing this to create simple refs from expressions that don't require concurrent execution, consider using the preceding form
All the refs above are of type int:
. As can be seen above, the key when creating refs is the use of :
postfixed to a type or prefixed to the assignment operator =
- this tells the compiler that we wish to create a ref type.
Refs enable us to do two very useful things core to the operation of most programs written in Concurnas:
They provide a mechanism by which differing isolates (pieces of code which are executed at the same time) can communicate between each other. For now, consider that isolates are concurrently executed blocks of code indicated by post-pending the block with the isolate creation bang !
. e.g {1+1}
!
They enable reactive computing, via use of onchange, every, async and await (this topic is explored later in the Concurrency section).
Refs have posses an very important feature which helps to enable the above. When we wish to perform an operation on a ref variable, or pass a ref to a function which is not expecting a ref - the ref will be unassigned. But, if no value has yet been assigned to the ref further execution will be blocked until a value is available.
It is for this reason that the following code will fail at compilation time with an error:
a int:
b = a //fails as a has not been assigned and and we will never be able to unassign from a to b, causing us to wait forever
Type wise, when unassigning a ref with type X:
, the type of the value returned will be X
. For int:
this is int
. For example:
a int:
{a = 99}! //execute our ref assignment in a separate isolate
b int = a //a is unassigned when our isolate spawned above completes execution
If we wish to prevent this unassignment we can use the :
operator when referencing the variable. For example:
a int: = 12
b = a:
c := a
//the &== operator tests for equality by object reference and not by object value as the == operator does
assert b: &== a: //a and b are references to the same ref
assert c: &== a: //a and c are references to the same ref
Actors?
Actors are covered in more detail in the Actors section. In summary, they are a hybrid between isolates and objects.
Method calls on actor objects occur within one dedicated isolate for the actor. Only one method is executed per actor at any point in time, as such actors can be shared between isolates. Method calls to actor methods return refs. An example actor:
class NormalClass(counter = 0){
def inc() => ++counter
}
anActor actor NormalClass = new actor NormalClass()
anotherActor = new actor NormalClass()//the type of anotherActor is inferred as: actor NormalClass
aref = anActor.inc() //aref is infered as type int:
All actors are a subtype of actor
which is itself a shortcut for com.concurnas.lang.Actor
:
class MyClass()
act1 actor = new actor MyClass()
act2 com.concurnas.lang.Actor = new actor MyClass()
Pass by value/reference?
Concurnas applies pass by value or pass by reference contingent on the type being referred to. Primitive types are passed by value, everything else (including primitive type arrays) are passed by reference. Therefore the following conditions hold true:
Concerning primitive types, a copy of the value is made:
anint1 = 99
anint2 = anint1 //copy value of anint1 to anint2
anint2++ //increment anint2
assert anint2 == 100
assert anint1 == 99
We see above that the original value held by variable anint1
is unchanged by the operation applied to the value of anint2
.
However, the behaviour is different with objects:
class MyClass(~avar int)
mcvar1 = MyClass(99)
mcvar2 = mcvar1
mcvar2.avar++
assert mcvar1.avar == mcvar2.avar
We see here that the internal state of the object referred to by mcvar1
is the same as that of mcvar2
- this is because a reference has been passed to mcvar2
upon assignment - a copy of the value of mcvar1
is not made.
Sometimes this behaviour is undesirable and we'd want mcvar2
to be a copy of the mcvar1
. This can easily be achieved by tweaking our code above to include the copy operator @
:
class MyClass(~avar int)
mcvar1 = MyClass(99)
mcvar2 = mcvar1@
mcvar2.avar++
assert mcvar1.avar <> mcvar2.avar
Above we see that the state of mcvar1
and mcvar2
is no longer the same, as they refer to different objects.