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.