Generics are a different way to reuse code for different types.
We can use interfaces for this, but interfaces have some limitations.
We often want to reuse a function for multiple types. On the interfaces page, we used an interface to allow the function to operate on any type that conformed to a certain contract.
A function that takes an interface doesn't have to care about the original type of the substruct we pass in.
However, that can also be its downside: it forgets the original type of the substruct we pass in. This can be mighty inconvenient, like in this example.
Notice how main is giving two Firefly to longerShip, so we know that longerShip should return a Firefly.
main doesn't know that though; all is sees is that longerShip returns an &ISpaceship.
Because of this, we can't call crazyIvan on the longer ship!
interface ISpaceship {
func length(virtual this &ISpaceship) int; 0
func honk(virtual this &ISpaceship) void;
}
func longerShip(
a &ISpaceship,
b &ISpaceship)
&ISpaceship {
if (a.length() > b.length()) {
return a;
} else {
return b;
}
}
struct Raza { ... }
func length(r &Raza) int { 4 }
func honk(r &Raza) { ... }
impl ISpaceship for Raza;
struct Firefly { ... }
func length(f &Firefly) int { 3 }
func honk(f &Firefly) { ... }
impl ISpaceship for Firefly;
func crazyIvan(f &Firefly) { ... }
exported func main() {
flametail = Firefly(...);
serenity = Firefly(...);
ship = longerShip(
&flametail, &serenity);
// ship.crazyIvan();
// Compile error: ship is type
// ISpaceship, not Firefly!
}
In future versions, this can be shortened to just func length(this) int;.
We can make longerShip into a generic function.
Its parameters aren't exactly ISpaceship, they are any type T.
Since the parameters and return type are all T, the compiler knows that if we hand in Fireflys, we return a Firefly.
In this case, we're calling it like longerShip<Firefly>(...), which specifies that we should use the version of longerShip where T is a Firefly.
longerShip<T> returns a T, so main knows that longerShip<Firefly> returns a Firefly.
interface ISpaceship {
func length(virtual self &ISpaceship) int;
func honk(virtual self &ISpaceship);
}
func longerShip<T> 1
(a &T, b &T) &T {
if (a.length() > b.length()) {
return a;
} else {
return b;
}
}
struct Raza { ... }
impl ISpaceship for Raza;
func length(r &Raza) int { 4 }
func honk(r &Raza) { ... }
struct Firefly { ... }
func length(f &Firefly) int { 3 }
func honk(f &Firefly) { ... }
func crazyIvan(f &Firefly) { ... }
impl ISpaceship for Firefly;
exported func main() {
flametail = Firefly(...);
serenity = Firefly(...);
ship =
longerShip<Firefly>( 2
&flametail, &serenity);
// This works now!
ship.crazyIvan();
}
In future versions, this can be <T impl ISpaceship> which will give a better compile error if someone calls longerShip with a non-ISpaceship.
We can actually leave off the <Firefly> here, the compiler can figure it out from the arguments we passed in.
We can make generic structs too.
Here we're making a Flock struct that can hold multiple of the same ship.
// Using above imports and
// ISpaceship, Firefly, Raza
struct Flock<T> {
ships List<T>;
}
exported func main() {
f = Flock<Firefly>(List<Firefly>());
f.ships.add(Firefly(...));
f.ships.add(Firefly(...));
firstShip = f.ships.get(0);
// This works because the compiler
// can see that firstShip is a
// Firefly.
firstShip.crazyIvan();
}
Next: Patterns