Chapter 6: Generic programming Up Main page Chapter 8: Special programming patterns 

7 Object-oriented programming

In Quasar, there are three types of classes:
The distinction between class and mutable class enables the compiler and run-time to make stronger assumptions on the constantness of the corresponding objects, potentially resulting in a more efficient execution.
Furthermore, classes of the type class and mutable class can be used from host, device and kernel functions. Dynamic classes can only be used from host functions.
Another difference between class and dynamic class is in the null values. For dynamic class, a null reference null is used. For class and mutable class, a null pointer nullptr needs to be used.

7.1 Mutable/non-mutable classes

Mutable/non-mutable classes require all members to be statically typed. Dynamically typed members are not supported, because they can not be mapped onto static types on the computation device.
However, it is possible to define parametric types, in which the dynamically typed members are replaced by a parameter type (see further). Then the parametric type needs to be instantiated (either directly, or via function specialization), to be used on the computation device.
A example of a mutable class, with a few member functions is given below:
type point : mutable class
x : scalar
y : scalar
endtype
Recursive types can also be defined, although, the recursive member needs to be a pointer type (^). For example, the definition of a linked list of points can be as follows:
type point : mutable class
x : scalar
y : scalar
next : ^point
endtype

7.2 Constructors

A constructor can be added to the point class. The following constructor uses the default constructor point(x:=xval, y:=yval) to initialize all object members.
function y = point(px : scalar, py : scalar)
y = point(x:=px, y:=py)
endfunction
Constructors can be overloaded. A constructor that is intended to be used from kernel/device functions should have the __device__ modifier:
function y = __device__ point(px : scalar, py : scalar)
y = point(x:=px, y:=py)
endfunction

7.3 Destructors

Due to the automatic memory management, there are no destructors. Destructors may be added in a future version of Quasar.

7.3.1 Methods

To define methods, Quasar uses a pattern similar to Google Go. A method is a function for which the first parameter is self, referring to the object on which the method is called. The self object can be passed by-value (without a pointer ^), or by reference (using the pointer ^).
function y = scale(self : point, b)
y = point(x:=b*self.x,y:=b*self.y)
endfunction
% The method point.setLocation
function [] = setLocation(self : ^point, x, y)
self.x = x
self.y = y
endfunction
% The method point.translate
function [] = translate(self : ^point, dx, dy)
self.x += dx
self.y += dy
endfunction
% The method point.tostring
function y = tostring(self : point)
y = sprintf("(%f,%f)",self.x,self.y)
endfunction
Methods can be called in the same way as in other object-oriented languages. For example:
p = point(1.0, 2.0) % constructor
print p.scale(2) % method
Finally, methods can be overloaded. A method that is intended to be used from kernel/device functions should have the __device__ modifier.

7.3.2 Properties

Properties can be added to the class, using reductions. The following reductions define a getter and setter for the property length:
reduction (a : point) -> a.length = sqrt(a.x ^ 2 + a.y ^ 2)
reduction (a : ^point, b : scalar) ->
(a.length = b) = (a = point(x:=b/a.length*a.x,y:=b/a.length*a.y))

7.3.3 Operators

Similarly, operators can be defined. For example, to calculate the difference between two points, one could define:
reduction (a : point, b : point) -> a - b = point(a.x-b.x,a.y-b.y)

7.4 Dynamic classes

Dynamic classes are very useful for scripting. Consider the following dynamic class definition:
type Bird : dynamic class
name : string
color : vec3
endtype
At run-time, it is possible to add fields or methods:
bird = Bird()
bird.position = [0, 0, 10]
bird.speed = [1, 1, 0]
bird.is_flying = false
bird.start_flying = () -> bird.is_flying = true
Dynamic classes are also enable easy interoperability with other languages (e.g., C#, Visual Basic). Dynamic classes are also frequently used by the UI library (Quasar.UI.dll).
Despite the fact that dynamic classes can have properties that are added at run-time, the compiler still performs type inference on them, resulting in efficient code.
One limitation is that dynamic classes cannot be used from within __kernel__ or __device__ functions. As a compensation, the dynamic classes are also a bit lighter (in terms of run-time overhead), because there is no multi-device (CPU/GPU/\SpecialChar ldots) management overhead. It is known a priori that the dynamic objects will “exist” in the CPU memory.

7.5 Parametric types

A disadvantage of non-static types is that the compiler may not be able to determine the types of the members of the class.
type stack : mutable class
tab
pointer
endtype
In this case, the compiler cannot make any assumptions w.r.t. the type of tab or pointer. When objects of the type stack are used within a for-loop, the automatic loop parallelizer will complain that insufficient information is available on the types of tab and pointer.
Parametric types can be used to solve this issue:
type stack[T] : mutable class
tab : vec[T]
pointer : int
endtype
An object of the type stack can then be instantiated as follows:
obj = stack[int]()
obj = stack[stack[cscalar]]()
It is also possible to define methods for parametric classes:
function [] = __device__ push[T](self : stack[T], item : T)
cnt = (self.pointer += 1) % atomic add for thread safety
self.tab[cnt - 1] = item
endfunction
Methods for parametric classes can be __device__ functions as well, so that they can be used on both the CPU and the GPU.
The internal implementation of parametric types and methods in Quasar (i.e. the runtime) uses a combination of erasure and reification.
Defining a constructor is based on the same pattern that we used to define methods. For the above stack class, we have:
function y = stack[T]()
y = stack[T](tab:=vec[T](100), pointer:=0)
endfunction
% Constructor with int parameter
function y = stack[T](capacity : int)
y = stack[T](tab:=vec[T](capacity), pointer:=0)
endfunction
% Constructor with vec[T] parameter
function y = stack[T](items : vec[T])
y = stack[T](tab:=copy(items), pointer:=0)
endfunction
Note that the constructor itself creates an instance of the type, rather than that it is done automatically. Consequently, it is possible (although it should be avoided) to return a nullptr value as well.
function y : ^stack[T] = stack[T](capacity : int)
if capacity > 1024
y = nullptr % Capacity too large, no can do...
else
y = stack[T](tab:=vec[T](capacity), pointer:=0)
endif
endfunction
Operators/properties on parametric classes can be defined using parametric reductions. In a parametric reduction, the type parameter itself is part of the parameter list of the reduction.
type point[T] : mutable class
x : T
y : T
endtype
reduction (T, a : point[T], b : point[T]) -> a - b = point[T](a.x-b.x,a.y-b.y)
Note: it is currently not possible to define constraints on the type parameters. This functionality may be added in a future version of Quasar.

7.6 Inheritance

Inherited classes can be defined as follows:
type bird : class
name : string
color : vec3
endtype
type duck : bird
...
endtype
Inheritance is allowed on all three class types (mutable, immutable and dynamic).
Note: multiple inheritance is currently not supported.
As an example, consider the following point, line and circle classes:
type geometry : mutable class
color : scalar
endtype
type point : geometry
x : scalar
y : scalar
endtype
type line : geometry
p1 : point
p2 : point
x1 : scalar
y1 : scalar
x2 : scalar
y2 : scalar
endtype
type circle : point
radius : scalar
endtype
function y = distance_from_origin(p : ^point)
y = sqrt(p.x^2 + p.y^2)
endfunction
c = circle(color:=0, radius:=4, x:=12, y:=5)
g = geometry(color:=1)
p = point(x:=4, y:=3, color:=1)
print "point distance from origin: ", distance_from_origin(p) % result=5
print "circle center distance from origin: ", distance_from_origin(c) % result=13

7.7 Virtual functions, interfaces, abstract classes

Virtual functions, interfaces, abstract classes are currently not supported by Quasar. These concepts may be implemented in a future version.
As a simple alternative of an interface, function types can be used. This way, it is possible to ‘emulate’ interfaces in Quasar:
type my_interface : mutable class
times2_function : [__device__ scalar -> scalar]
sum_function : [__device__ vec -> scalar]
do_something : [(^my_interface) -> ??]
endtype
obj = my_interface(
times2_function := (__device__ (x : scalar) -> 2*x),
sum_function := (__device__ (x : vec) -> sum(x)),
do_something := (self : ^my_interface) -> print(self)
)
print obj.times2_function(2)
print obj.sum_function([1,2,3])
obj.do_something(obj)
In the same way, abstract classes and virtual functions can be emulated. An advantage is that this technique works across computation devices, with no additional compiler support.


 Chapter 6: Generic programming Up Main page Chapter 8: Special programming patterns