Models
Models are Bosscript’s approach to Object-Oriented Programming (OOP). A Model encapsulates data and behavior, and is a template
for creating objects - instances of the Model. In most programming languages, the keyword class
is used, but in Bosscript
the keyword model
is used. This is because the word class
(translated as klasa
) is understood a bit differently in Bosnian,
and model
does a better job of conveying what they’re used for.
A model definition consists of the following components:
- The model name
- Optional parent model name
- Model body
Model Name
Model names follow the same rules as variable names. However, the convention is to capitalize them, i.e, to use UpperCamelCase. A model name cannot coincide with an existing identifier (function or variable name).
var x = 10; model x {...}
Error: x has already been defined.
The example below shows the conventional way of naming models:
model Example{...} model ExampleMultipleWords{...}
Model Body
The model body consists of three components:
- The private block
- The public block
- The constructor
Only the constructor is mandatory. The private and public blocks can be omitted if there is no need for them. The order in which the three appear also doesn’t matter. The only rule is that there can be only one of each.
Consider the following example of a very basic model:
model User{ privatno { var name; } javno { funkcija getName(): tekst { vrati @name; } } konstruktor(name: tekst){ @name = name; } }
Writing anything other than the private block, public block or constructor within the model body will result in an error:
model Example { za svako(x od 1 do 10){ ... } }
Error: Expected private or public block, or constructor. Got 'ForStatement'.
Constructors
As mentioned previously, each model must have a constructor, and it can only ever have one. Except for the konstruktor
keyword,
it behaves similar to a regular function. However, keep in mind that a constructor cannot return anything. Depending on the
version you are using, Bosscript might allow you to write return statements inside a constructor, but these statements will have no effect.
You might think that allowing only one constructor is too limiting, but anything that can be achieved with multiple constructors can also be achieved with one:
model Example{ konstruktor(a, b, c){ ako(!a){ @a = "PLACEHOLDER"; } inače { @a = a; } ako(!b){ @b = 10; } inače { @b = b; } ako(!postoji(c)){ @c = "nedefinisano"; } inače { @c = "definisano"; } } }
The example above shows a constructor that takes three arguments, where each argument can be undefined (nedefinisano
).
This means that any combination of (a
, b
and c
) can be passed. This is the equivalent of having 6 different constructors.
A constructor can initialize properties that were not declared as variables in the private or public blocks:
model Example { privatno { var x; var y; // No var z declared } konstruktor(x, y, z){ @x = x; @y = y; @z = z; // Works just fine regardless } } var e = Example(3, 5, 2);
However, we recommend explicitly declaring all properties, for the sake of clarity and readability. Also, properties that were not declared explicitly are considered public by default, which is something you might not want.
Notice that there is no new
keyword when calling a constructor. It is enough to use the name of the model as a callable:
// JavaScript const example = new Example();
// Bosscript var example = Example();
The @
keyword
You’ve probably noticed the @
keyword being used extensively in the constructor examples above. @
is used to refer to
the instance, i.e., it is the equivalent of this
. Writing <span class="purple">@a</span>
in Bosscript is the equivalent of writing this.a
in Java
and other similar languages.
Usage of @
is not limited to constructors. You can use it anywhere within the class to refer to the instance:
model Example{ privatno { var x; var y; } javno { funkcija test(){ vrati @x * @y; } } ... }
In fact, referring to model properties without @
will result in an error:
model Example{ privatno { var x; var y; } javno { funkcija test(){ vrati x * y; } } ... }
Error: x does not exist
Private and Public Block
Having the private and public blocks is Bosscript’s way of implementing visibility/accessibility. Instead of having to write an access modifier in front of every variable and function, Bosscript users simply use the appropriate blocks. This syntax was inspired by C++, but unlike C++, Bosscript uses real blocks delimited by curly braces. This also encourages users to organize their code in a consistent matter, and helps them visually separate the functionality of their models.
// C++ class Example{ private: int x; int y; public: Example(int x, int y){ this.x = x; this.y = y; } }
// Bosscript model Example{ privatno { var x; var y; } konstruktor(x: broj, y: broj){ @x = x; @y = y; } }
Within both the private and the public block, only variable and function declarations are allowed. Other statements will cause an error:
model Example { privatno { za svako(x od 1 do 10){ ... } } }
Error: Expected model member declaration, got 'ForStatement'.
All variables and functions declared in the private block are inaccessible from the outside, while all variables and functions declared in the public block can be accessed by anyone anywhere.
var user = User("Bosscript"); ispis(user.name);
Error: 'name' is private.
In the example model User
, name
was declared in the private block, and trying to access it outside the class causes
an error. On the other hand, getName
was declared in the public block, so it can be accessed just fine:
ispis(user.getName());
Output: Bosscript
Inheritance
A model can extend another existing model. This is called inheritance. Under the hood, Bosscript utilizes prototype-based
inheritance, but the concepts are exactly the same as in most programming languages. A model can extend only one other model.
There is no support for multiple inheritance. There is no keyword like extends
, instead use the <
symbol. This syntax
was inspired by Ruby:
model A { ... } model B < A { ... }
A model cannot extend a type definition:
tip T { x: broj; } model M < T{ ... }
Error: Expected Model Definition, got Type Definition @5:11
Super calls
The constructor of a subclass must call the parent class constructor. That is usually the rule in most programming languages.
That rule also applies to Bosscript. However, the keyword used is not super
, but rather prototip
. This is both because
the underlying prototype-based inheritance and the fact that the word super in Bosnian is only used to say great. It’s needless
to say that supermodel
would not be a good choice of keyword.
The prototip
call must be the first expression in the inheriting model’s constructor. Trying to access @
before calling
prototip()
will result in an error:
model A { privatno { var x; var y; } konstruktor(x, y){ @x = x; @y = y; } } model B < A { privatno { var z; } konstruktor(x, y, z){ @x = x; @y = y; @z = z; } }
Error: Constructors for derived classes must contain a 'prototip' call.
The proper way to do it is shown below:
model A { privatno { var x; var y; } konstruktor(x, y){ @x = x; @y = y; } } model B < A { privatno { var z; } konstruktor(x, y, z){ prototip(x,y); @z = z; } }
Private fields in inheritance
In Bosscript, a child model has access to all private fields of its parent model. In Java terms, private fields in Bosscript behave
more like protected
fields. You can access any private field of any model that is higher up in the inheritance chain simply
with the @
reference:
model A { privatno { var x; var y; } ... } model B < A { privatno { var z; } javno { funkcija test(){ vrati @x * @y * @z; } } }
Observe the test
function in the code snippet above. It references x
and y
of the parent model A
directly. This is
completely valid Bosscript code that will run without errors. The private block is only inaccessible from outside the model
and its children.
Overriding methods and fields
A child model may override a method of its parent model. To do this, simply declare a method by the same name as in the parent model and provide a new implementation:
model A { javno { funkcija test(){ vrati 10; } } } model B < A { javno { funkcija test(){ vrati 20; } } } var b = B(); b.test();
Output:
20
You can call the parent’s implementation of the method you are overriding. To do so, you will need to reference the
__roditelj__
object, which represents the parent model.
model A{ ... javno { funkcija test(){ vrati @x * @x; } } } model B < A { ... javno { funkcija test(){ vrati (@y * @y) + @__roditelj__.test(); } } } var b = B(4, 3); ispis(b.test());
Output:
25
In the example above, model B
overrides model A
’s implementation of the test
function. B
’s implementation of test
calls A
’s implementation with the expression @__roditelj__.test()
. That expression evaluates to 16, which is added to
9, the result of (@y * @y)
. The final result is 25.
It is also possible to override model fields:
model A{ ... javno { var overriden = tačno; } } model B < A { ... javno { var overriden = tačno; } } var b = B(); ispis(b.overridden);
Output:
tačno
In the example above, model B
overrides the field overriden
.
Operator overloading
Bosscript models support operator overloading for most binary operators. To overload an operator, you need to implement a public function under a specified name. Here is the comprehensive list:
Operator | Function Name |
---|---|
+ | plus |
- | minus |
* | puta |
/ | podijeljeno |
< | manjeOd |
> | veceOd |
= | jednako |
!= | nijeJednako |
Here is an example of how you might implement all of these operators in a model:
model DupliBroj { konstruktor(x, y){ @x = x; @y = y; } javno { var x, y; funkcija plus(drugi: DupliBroj){ vrati DupliBroj(@x + drugi.x, @y + drugi.y); } funkcija minus(drugi: DupliBroj){ vrati DupliBroj(@x - drugi.x, @y - drugi.y); } funkcija puta(drugi: DupliBroj){ vrati DupliBroj(@x * drugi.x, @y * drugi.y); } funkcija podijeljeno(drugi: DupliBroj){ vrati DupliBroj(@x / drugi.x, @y / drugi.y); } funkcija manjeOd(drugi: DupliBroj): logicki { vrati (@x < drugi.x) && (@y < drugi.y); } funkcija veceOd(drugi: DupliBroj): logicki { vrati !@manjeOd(drugi); } funkcija jednako(drugi: DupliBroj): logički { vrati (@x == drugi.x) && (@y == drugi.y); } funkcija nijeJednako(drugi: DupliBroj): logički { vrati !@jednako(drugi); } } }
Keep in mind that manjeOd
, veceOd
, jednako
, and nijeJednako
implementations must return a logički
value. Other
operator functions may return a value of any type, but most of the time, you will want to return an instance of the containing model.
All operator functions can have only one parameter. The parameter can be of any type, but most of the time you will want to accept another instance of the containing model. Sometimes it might make sense to accept an argument of another type:
model Vector{ ... privatno { var arr = []; } javno { funkcija plus(right){ ako (right.tip == "broj"){ arr.zaSvaki(funkcija(it){ it += obj; }); } ili ako (right.tip == "Vector"){ @arr = arr.spoji(right); } inače { greska("Incompatible type.") } } } }
In the example above, the plus
function argument is not typed at all. It can accept an argument of any type, but broj
and Vector
are the types we want to see. Using a simple if-statement, we define the desired behavior for each of those
types, while passing any other type will result in an exception being thrown.