Template-based Generics
One of the most important aspects of the Ryna type system are templates. These allow you to pass types as parameters in order to create other types. This is mainly used in function and class definitions to prevent code repetition. In this section, we will only explain the basic syntax and semantics of templates.
Introduction
A template can be defined as a placeholder for another type to go into. They allow the programmer to write typed structures such as "lists of T", where T is a type with certain properties without the need of rewriting the class for each possible T. The implementation can be a litle more involved, but it pays off as a library writer.
Syntax
Type templates are specified in the context of structural types, classes, functions and operations. Each of these has their own syntax and diving into the specifics would be outside the scope of the type system, but we will talk about the syntax when templates are already defined.
Templates are always represented with an single quote character before an alphanumerical identifier that follows the same rules as
class names. Some examples of valid template names would be 'Test
or 'Parameter_1
. Names such as '0123
or names with non-ASCII
characters are not allowed.
Now, this is the way to reference a template that has already been defined, but when you have class that depends on one or more templates (i.e.
a parametric class), you also have to specify the type parameters when referring to the class. For example, you cannot refer to the Array
class
without specifying the type of data that goes inside the array as the first (and only in this case) type parameter. These parameters are specified
the same way as in languages such as C++, with angle brackets. These would be some examples:
Array<Int> // Array of Ints
Array<Float | String> // Array of either Floats or Strings
Array<*> // Array of anything
Array<'T> // Array of 'T (only if 'T is in scope)
Of course, we can define a class with an arbitrary amount of type parameters, but Ryna does not include any with more than one by default. If we had an
hypotetical class called HashMap
that had two parameters (the type of the key and the type of the value), we could refer to it like this:
HashMap<Int, Int> // HashMap from Ints to Ints
HashMap<String, Float | String> // HashMap from Strings to either Floats or Strings
HashMap<*, *> // HashMap from anything to anything
HashMap<Bool, 'T> // HashMap from Bool to 'T (only if 'T is in scope)
Semantics
Like we said before, a template is not exactly a type, but a placeholder for a type that will be substituted by a proper type later. This is done to prevent manual code repetition and allow the creation of more general code.
How this works is pretty simple. Every time you create a parametric class and refer to it anywhere in the code, the interpreter will substitute the parameters automatically for the ones given inside the angle brackets. This can be essentially understood as creating a new class for each distinct instance of a parametric class. This mechanism works exactly the same way for parametric functions, structural types and operations.
Of course, the fact that you substitute a type does not mean that the code magically works. In fact, it might fail to compile if you try to call a function overload that does not exist. The main way to avoid this is bounded substitution via interfaces.
Bounded substitution
Note: this works as of now, but syntax will probably change in future releases
This mechanism allows the programmer to specify constraints while defining a parametric type instance. One example of why you would want to do this is that
HashMap
class we discussed earlier. As you might know, a HashMap structure requires keys to be hashable because of how data is stored inside for fast
retrieval, but not all types might be hashable and you would also want to user to know that they have to implement a hashing function for a custom type
before they can put it as a HashMap key.
This is what interfaces do, they allow you to define APIs and assert whether or not a type follows it or not. If you define an interface called Hashable
that checks whether or not the type that implements it (called Self
) has a function called hash
that takes a Self
and returns an Int
, you can use it as a
constraint for template substitution.
This woult be done simply by changing the references to 'T
inside the class definition to 'T [Hashable]
, which translates to a T which is Hashable. You can put as
many constraints as you want separated by commas and the constraints can also be parametric, following the same syntax rules as types.