Type safety at compile time

In many languages, the compiler verifies that your code is well-typed, that is, there are no type errors that might cause a malfunction or raise an exception at run time. The code examples below are written in Mojo, a superset of Python.

Typed functions

In a typed function, you must explicitly specify the types of its arguments and its return value. For example:

fn factorial(n: Int) -> Int:
	if n <= 1:
		return n
	return n * factorial(n - 1)

The signature of the factorial function specifies that its argument is an integer and it returns an integer. So the expression factorial(5) gets compiled but factorial(5.0) doesn't:

error: invalid call to 'factorial': argument #0 cannot be converted from 'FloatLiteral' to 'Int'

Typed structures

Structures (composite types, similar to classes) have typed fields, for example:

@value
struct Person:
	var name: String
	var age: Int

An instance can then be created using a typed constructor, for example:

var p = Person("John", 30)

but var p = Person("John", 3.0) leads to a compile-time error:

error: invalid initialization: argument #2 cannot be converted from 'FloatLiteral' to 'Int'

Parameters

Some types, such as collections, have type parameters to specify their elements' type, for example:

var list: List[Int] = List[Int](1, 2, 3)

This statement creates an instance of a list of integers with three elements. In many cases, parameters can be inferred from the expression, so a more succinct way to write the statement is:

var list = List(1, 2, 3)

Traits

The type system described so far is quite rigid. To make it more flexible (or dynamic), traits were introduced (in other languages they're called interfaces or protocols). For example:

trait Named:
	fn get_name(self) -> String: ...

Every type, that conforms to the Named trait, must provide a get_name method. To make our Person type conform to it, it may be extended as follows:

@value
struct Person(Named):
	var name: String
	var age: Int

	fn get_name(self) -> String:
		return self.name

Now a type parameter can be used to define a function, say print_name, that can be called with any argument whose type conforms to the Named trait:

fn print_name[T: Named](x: T):
	print(x.get_name())

Variant

Another way of making the type system more flexible is having a Variant type. For example:

var x: Variant[Int, String] = 1234

The Variant[Int, String] type can take values of type Int and String. Unfortunately such types are somewhat unwieldy to work with:

if x.isa[Int]():
	var n: Int = x[Int]
	print(n)

Type inference

If all functions, structures and methods are equipped with well-typed signatures, the code that uses them rarely needs to include explicit type annotations because types can be inferred in most cases. This makes the code look more like a language with optional or absent type annotations, which improves readability.

TypesMojo
Avatar for Petr Homola

Written by Petr Homola

Studied physics & CS; PhD in NLP; interested in AI, HPC & PLT

Loading

Fetching comments

Hey! 👋

Got something to say?

or to leave a comment.