Generic

While using things demonstrated until here we can describe almost anything we want using Fuse, There is still a crucial tool missing from our tool belt. At times we want to generalize our types without loss of any type data. One may implement such a thing using a union, compound types via type-or(|) operator or unsafe type. All these can partially solve our problem or not having a way for generalized types but each of them comes with its own cons. 1. An union needs an extra type definition which also produces redundant type data 2. The type or operator also needs extra type definition but instead of redundant type data, it erases some information which may cause runtime panics. 3. An unsafe type can be used to make a type that can accept any value, But as a result, it would erase all type information and make our code more error-prone.

A generic type can be used in place of the solutions mentioned above to overcome the limitations of all of them combined, With generics we can define type parameters that can be substituted with the desired type by the compiler.

fn func<T>(e: T) -> T
  -- ...
end

let n: number = 12345
let m: number = func<number>(n)
-- or use type inference
let m = func(n)

Note: One of the greatest places to use generic data types is in the collections, All of the collection types in Fuse are implemented using generic types.

Fuse allows generic functions, structs, traits, unions, implementations, type aliases, and tables, As we have seen earlier a generic function allows the use of type parameters as both parameter type and/or return type; Next, we are going to explore the other generic types.

Generic structs

We can have generic structs, These structs can have one or more type parameters which then can be used to define their fields.

struct Node<T>
  value: T
  children: Node<T>[]
end

An impl block can also take a type parameter and pass it along to its target type.

impl<T> Node<T>
  pub fn new(value: T, children: Node<T>[]) -> Self
    Self { value, children }
  end
end

Generic traits

A generic trait is defined similarly to a generic struct, Type parameters defined in a generic trait can be used for its methods.

trait NodeLike<T>
  fn children(self) -> T[]
  fn add_child(self, child: T) -> ()
end

Like for structs, we can also define type parameters for an impl block which then can be passed into our trait and/or structure.

impl<T> NodeLike<T> for Node<T>
  fn children(self) => self.children
  fn add_child(self, child: T) => self.children.add(child)
end

Generic unions

The type Optional is a generic union, Here is a simpler version of such type.

union Optional<T>
  None
  Some(T)
end

Generic unions can be implemented using generic implementations.

impl<T> Optional<T>
  fn unwrap(self) -> T
    match self when
      Some(value) then value end
      None then panic() end
    end
  end
end

Generic type aliases and tables

While tables don’t actually have a generic type and only can have generic functions it is possible to create a generic representation of table types using generic type aliases.

type MyTable<T> = { [T]: string }

let my_table: MyTable<number> = { [1]: "One", [2]: "Two", [3]: "Tree" }

Generic type aliases also let us specify some type parameters while keeping others generic.

type MyBackend<T, U> = Backend<T, U, Service>
type MySpecificBackend<T> = MyBackend<T, Database>
type MyNonGenericBackend = MySpecificBackend<Router>

Generic constraints

Sometimes having the most generic types doesn’t provide as much value as a more narrow type. We can add some constraints to our type parameters to make sure all allowed type parameters have some specific behavior that we would rely on.

Let’s say we want a function that only accepts arguments that can be iterated over. We can do this by constraining our type parameter to have an IntoIterator trait. This way the compiler makes sure that we are only allowed to pass values that would satisfy such constraint and allows us to use anything implemented in an IntoIterator trait.

fn loop_and_print<T: IntoIterator>(it: T) -> ()
  for i in it do
    print(i)
  end
end

Now we can only pass values that implement the IntoIterator trait.

let arr = [1, 2, 3]
loop_and_print(arr)
loop_and_print("Hi") -- Error, it won't compile!

When we have more than 1 constraint for one type parameter we can use compound types using a type-or(|) operator. Let’s say in our last example we want to have support for Iterator types in addition to types that implement the IntoIterator trait. We can implement such a function like this.

fn loop_and_print<T: IntoIterator | Iterator>(it: T) -> ()
  for i in it do
    print(i)
  end
end

Default types

A generic type parameter can have a default type, All parameters without a default type should come before the ones with a default value.

fn func<T, U, W = number>(a: T, b: U, c: W) -> ()
  -- ...
end