Structs are a very fundamental concept in Mochaccino. They are used to define custom types and interfaces, and offer very fine-grained control over your data. So buckle up, because this is going to be a long section.
Constructor
What does the constructor in an OOP language do? It initialises class properties and performs an action when the class is first instantiated. Constructors in Mochaccino play a similar role, by taking care of type conversion.
Type Assertions
A struct can have a constructor, which is empty by default, that converts data to its own type. The constructor is called whenever a type conversion (aka type assertion) is taking place. A type conversion looks like this:
12::<string>
On the left is the value that needs to be converted and on the right is the conversion target type. In this case, the constructor for <string> is called with 12 as the argument.
Note
Implicit type assertions also take place during variable intialisation.
Constructor
So what does a constructor look like? It looks like a method defined for that struct:
structstring {
constructor(data<dynamic>)<string> {
returnString.toString(data);
}
}
The constructor for this struct can take a value of any type and convert it to a string via the toString() method from the String module. But what’s with the return keyword? Well, the return keyword returns the elementary value of the struct. If you have no idea what an elementary value is, that’s okay, because that section comes
. All you need to know is that if you log a class instance to the console, it will print the value returned by the constructor.
structstring {
constructor(data<dynamic>)<string> {
returnString.toString(data);
}
}
structxyz {
var someProp<int>;
constructor(data<dynamic>)<void> {
someProp = data;
}
}
var a<string> = 1::<string>;
var b<xyz> = 1::<xyz>;
Console.log(a); // "1"
Consoe.log(b); // null
Also, you may have noticed that the constructor can choose which types can be converted to their type.
structMyJSON {
var someProp<int>;
constructor(data<map>)<void> {
someProp = data;
}
}
guarded Keyword
The guarded keyword in Mochaccino causes the succeeding statement to only be executed if it does not throw an error. If an except clause is specified, then the offending statement will be run, and the error will be handled by the except clause. This is typically used in type assertions, where one type is converted to another.
guarded12::<Service> exceptpause; // program exits without errors
guarded12::<Service>; // no type conversion takes place
Props
Props (short for properties) are used to define getters and setters in structs. But wait, what's the difference between properties and fields in a struct? A field in a struct is private by default, and not visible outside the struct. Hence, to make properties accessible and modifiable from outside the struct, you need to define properties. This is done using the prop keyword followed by the name of the property.
structCustomNumber {
prop isEven {
get<bool> {
return ((this % 2)==0);
}
}
}
Here we see that we've created a custom struct for our own type of numbers. It has a property isEven that tells us whether the number is even or not. We can access that property because a getter is defined for it. In this case, it makes sense that a setter is not defined—you can't change whether a given number is even or not, as that depends on the number itself. Now let's look at a case where a setter needs to be defined.
structcustomNumber {
var usedNumbers<array<customNumber>> = [];
prop hasBeenUsed {
get<bool> {
return Array.contains(userNumbers, this);
}
set (value<bool>)<void> {
if (value==true) {
} elif (value==false) {
}
}
}
}
However, props can only be used by certain structs, as you’ll find out in the next section.
Struct Annotation
Now would be a good time to introduce to you struct annotations, which are annotations preceded by @. These are used in defining structs and their behaviour, such as whether they can define props.
@elementary
These are three annotations you absolutely must know when using structs. One of these three special annotations must be provided when defining a struct. The @elementary keyword allows the struct to behave like a primitive type, where a struct instance has an elementary value. Of course, it’s easier to understand with an example:
structJSON {
@elementary
}
var data<JSON> = someJsonData;
Console.print(data);
You see, the default value of data is the JSON data. You don’t have to create a field to store the actual data and then access it via dot notation. You could, if you wanted to, give it a custom elementary value as well. The constructor comes in handy, as the elementary value is the value returned by the constructor.
Elementary structs can only be inherited by other elementary structs, and can be used as type annotations.
this Keyword
The this keyword can be used within an elementary struct to access the elementary value.
@interface
In Mochaccino, structs can (and usually do) have abstract properties or methods. This is known as an interface, and is marked with the @interface annotation. Interfaces do not have an elementary value (because they define guidelines for more complex data). Interfaces can only be inherited by other interfaces, and can be used as type annotations.
structJSON {
@interface
var value<map>;
func doStuff(number<int>)<void> async;
var value2<map>;
var value3<map>;
}
Notice how the properties in an interface do not need to be initialised. These are known as abstract members. A struct that inherits this struct has to provide concrete implementations for these members:
structApiJSON extendsJSON {
var value<map> = {};
func doStuff(number<int>)<void> async {}
var value2<map> = {};
var value3<map> = {};
}
In some languages, when child classes override methods and properties, they are marked with @override, but not in Mochaccino. Mochaccino automatically knows, based on the name of the member, whether it is overriding the base class or not.
@protocol
But what if you wanted to define a guideline for modules? Struct-Module polymorphism is achieved through the @protocol annotation. Structs marked as protocols cannot be used as type annotations (obviously), and can only be inherited by other protocol structs or modules. Now, here’s the fun part. You can create members by typing in variables and methods as you would in a normal module, and you’re even allowed to define abstract implementations if needed.
structProtocolABC {
@protocol
var someVar = 10;
var anotherOne;
func someMethod()<void>;
func concrete()<void> {}
}
var data<JSON> = someJsonData;
Console.print(data);
You see, the default value of data is the JSON data. You don’t have to create a field to store the actual data and then access it via dot notation. You could, if you wanted to, give it a custom elementary value as well. The constructor comes in handy, as the elementary value is the value returned by the constructor.
Type Annotations
Type Arguments
A map is a data type which always has a left-hand side and right-hand side. In this case, we can specify types for each value to be more specific. We use a type annotation within a type annotation to do so:
var data<map<string, int>> = someMap;
Or you could do this if you had an array:
var data<array<string>> = someArray;
It gets even better when you have a map with an array of maps inside it:
var data<map<string, array<map<string, int>>>> = someMap;
Type annotations are wonderfully easy to read, aren’t they? Now, let’s try specifying a type for a list of maps that store a list of maps that store integers. Moving on to union types...
Union Types
Ever had a moment when you’re expecting a value from a function to be either of type X or Y? Union types to the rescue! A pipe symbol (|) can be used in a type annotation to specify a group of possible types.
var data<map|array> = jsonFromApi;
Struct Polymorphism
You’ve seen implements and extends being used for modules in the previous page. Same idea here, though there are more restrictions on how certain structs can be extended. For brevity’s sake, we’ll use the term inheriting to collectively refer to a child object implementing or extending a base struct.
Struct Inheritance Rules
Struct-Struct Inheritance
Elementary structs can only be inherited by elementary structs.
Abstract structs can only be inherited by abstract structs.
Unmarked structs (not marked with @elementary or @abstract) cannot be extended.
Struct-Module Inheritance
A module can only inherit from an abstract struct.
A module can have the same name as the struct it inherits from (as long as that name isn’t used by another module).
Implementing Structs
If you’re implementing a struct, you’re saying that the new struct conforms and inherits fully the properties and methods of the base struct. The inheriting object must provide concrete implementations of the
Extending Structs
If you’re extending a struct, you’re saying that the new struct
Application of Structs
Custom Types
struct apiV4JSON {
@elementary
constructor(data<map>)<map> {
return data;
}
prop version {
get<int> {
return this['ver'];
}
}
prop map {
get<map> {
return this;
}
}
}
Adding Functionality
Now that we’ve defined some properties, lets make
Props VS Methods
Sometimes, you might find that the distinction between a prop and a method gets blurred. In the string package from the core library, the String module contains a toLowerCase method. You might wonder why this isn’t made a property instead. A property must meet the following criteria:
Getters of the property must not take any arguments
Getters of the property must not modify the object itself in any way (except for its private fields).
The moment any of the above criteria isn’t met, you’ll need to create a module that provides the methods for the struct. Refer to the