Generics

Generics, also known as templates, are special structures and functions which contain one or more variables with placeholder types. These placeholder types are replaced by actual types when the generic structure/function is created/used.

In modern programming, generics are one of the most useful features as they allow a programmer to write "generic code" which can be used with different types by replacing the placeholders.

Generics are the reason why data structures like vectors (dynamically sized arrays), maps (dictionaries), etc. are available in standard libraries of languages like C++. Since they use generic datatypes, they can work with any actual type the programmer requires.

Let's understand generics and their use.

Generic Structs

These structures contain one or more variables which have a "template"/generic type.

For example,

let GenA = struct<T> {
	data: T;
	ptr: *T
};

These structures cannot be instantiated directly. Instead, they must first be specialized to replace generic types with the actual types, and then initialized like any other structure.

For example,

let GenA = struct<T> {
	data: T;
	ptr: *T
};

let main = fn(): i32 {
	let a = GenA(i32){5, nil};
	return 0;
};

Here, we specialize GenA with i32 as the generic's replacement type, and then instantiate that with 5 and nil for data and ptr respectively.

Generic Functions

These functions contain one or more of either any type, or type type as a parameter.

With any, as previously described, any argument can be provided.

With type however, a specific type is utilized which can also be used elsewhere - say, as return type, variable declaration type, etc.

Combined with generic structures, we can also use a variable with type type to specialize a structure.

For example,

let GenA = struct<T> {
	data: *T;
};

let new = fn(comptime T: type): GenA(T) {
	return GenA(T){nil};
};

let main = fn(): i32 {
	let a = new(f32); // returns an instance of GenA, specialized with f32
	// a.data is of type *f32
	return 0;
};

All functions associated to generic structures are also generic functions themselves. Scribe provides the generic type as a member of the structure instance in these functions, accessible using self.

For example,

let GenA = struct<T> {
	data: *T;
};

let set in GenA = fn(newdata: *self.T): &self { // self can also be used as return type, which will become &GenA(self.T)
	self.data = newdata;
	return self;
};

let main = fn(): i32 {
	let a = GenA(i32){nil};
	let p: *i32 = nil;
	let q: *i32 = nil;
	a.set(p).set(q); // chaining is possible since set() returns &self
	return 0;
};

In Scribe, many data structures are made using generic structures and functions like:

Note that the generics are used and eradicated internally by the scribe compiler. That is, the generated C code (and subsequently the executable binary) will have no information of generics whatsoever.

It is also recommended to check out the examples and standard library of Scribe to understand more about generics if you understand the concepts well, as they can be used in a variety of ways and have their own nuances.

Conclusion

Here, we have learnt about generics and their usage in Scribe. We shall now move on to conditionals.