Skip to main content

Member functions

Hybrix classes can be used for object-oriented programming (OOP), a classic software engineering paradigm. It is a relatively deep topic, but the basic idea is simple: you add func members to your class, and then invoke them using the . operator. For example, similar to accessing a door.color member variable, we might call a door.open() member function.

The different kinds of member functions are explained below.

Func members​

Let's start with a basic example. The function below has a hidden parameter self that allows access to the current instance of the object.

class rectangle
var width: int
var height: int

func get_area(): int
return self.width * self.height
end func
end class

module main
func start()
var r: rectangle
new rectangle() -> r

# Accessing member variables
10 -> r.width
20 -> r.height

var a: int

# Calling a member function
r.get_area() -> a
end func
end module

Important points:

  • The func members must appear after any var members. For example, var width: int must come before func get_area(): int.
  • The . operator is used to call a member function like r.get_area(), just like how we would use r.width to access a member variable.
  • Inside that func, the special self variable returns the class instance. In our example, self refers to the same object as r.
  • Instead of writing self.width or self.get_area(), you can simply write .width or .get_area() for short.

Here's how class rectangle looks when using . instead of self.:

# (same meaning as the above definition)
class rectangle
var width: int
var height: int

func get_area(): int
return .width * .height
end func
end class

Constructors​

A class constructor is a special member function that is called automatically when using new to create an object. The constructor initializes the new object's member variables.

In the previous section we defined a circle class like this:

class circle
var x: int, y: int
var radius: int
end class

module main
func start()
var c1: circle, c2: circle

new circle() -> c1
0 -> c1.x
0 -> c1.y
5 -> c1.radius

new circle() -> c2
10 -> c2.x
20 -> c2.y
5 -> c2.radius
end func
end module

The statements such as 0 -> c1.x are tedious to write, and it's easy to accidentally forget one of them. Here's an equivalent program that uses a class constructor:

class circle
var x: int, y: int
var radius: int

# The class constructor:
constructor(x: int, y: int, radius: int)
x -> .x
y -> .y
radius -> .radius
end constructor
end class

module main
func start()
var c1: circle, c2: circle

# Create a new circle, invoking its constructor
new circle(0, 0, 5) -> c1

# Create a new circle, invoking its constructor
new circle(10, 20, 5) -> c2
end func
end module

Important points:

  • Class constructors are defined using constructor.
  • Class constructors are called automatically by expressions such as new circle(0, 0, 5)
  • Behind the scenes, the new expression calls an allocator function to allocate heap memory for the object, then the constructor is called for that object.
  • It might seem that a constructor's return value should be circle, but the constructor itself doesn't return anything. It receives the partially initialized object as its self parameter.
  • Note that when we write (new circle) to make a function pointer, its type is func(): circle. This is because it actually points to the allocator, not the constructor function. (Otherwise, how can the constructor use .x to reference the self object?)
  • Constructors are optional; if omitted, the compiler provides a default constructor with no function parameters.

Hook members​

Classes also support another kind of function defined using hook instead of func. They behave almost exactly the same as func. The difference has to do with inheritance, which we'll discuss in the next section.

Indexers​

If a class acts like a container of elements, it can be more intuitive to use an array-like notation to refer to its elements. The classic examples are list[i] (a resizable wrapper for an array) and map[key] <- value (a dictionary where values are looked up using a key). For example, a statement like my_map.set(key1, my_map.get(key2)) can be more intuitive as my_map[key1] <- my_map[key2].

The Hybrix language supports this syntax using indexers, declared using the special forms func get[](...) and func set[](...). In the example below, clamped_array behaves basically like an array, except it guarantees that the array elements are always within the range -100 to 100:

class clamped_array
var _items: int[]

# ("view var" prevents other classes from modifying "size")
view var size: int

constructor(size: int)
._items <- new int[](size)
.size <- size
end constructor

func get[](i: int): int
return ._items[i]
end func

func set[](i: int, value: int)
if value < -100 then
-100 -> value
elsif value > 100 then
100 -> value
end if
._items[i] <- value
end func
end class

module main
func start()
var a: clamped_array
a <- new clamped_array(5)
a[0] <- 50 # stores 50
a[1] <- 1234 # stores 100
a[2] <- -1234 # stores -100
end func
end module

Important points:

  • Reading from my_object[x] is equivalent to calling my_object.get(x).
  • Assigning my_object[x] <- y is equivalent to calling my_object.set(x, y).
  • The [] is not part of the function name; it merely enables the indexer syntax.
  • The get indexer must receive exactly one parameter and return a value.
  • The set indexer must receive exactly two parameters with no return value.
  • Both indexers are optional, but if you define set then you must also define get.
  • The index and value parameters can be any data type, but they must be consistent between get and set indexers.
  • In all other respects, indexers are normal member functions. For example, you can write (my_object.get) to make a function pointer.
  • The Hybrix language does not provide general operator overloading; indexers are the only user-defined operator-like syntax.