Basics
The first program to write is "Hello, world!". Here it is in Nara. Press the "Run" button to see the output.
print("Hello, world!")
Nara syntax is C-like, however, statements do not require semicolons at the end. Instead, newlines are used to separate statements.
print("Hello")
print("Bye")
What is Nara
Nara is a modern, safe, functional & strongly typed programming language.
- Functional first: it is design with functional programming in mind, immutable by default. However, Nara is not purely functional. It allows mutation where useful, but mutability is opt-in.
- Statically typed: all values in the language have a well defined type, at all times. This also means that the compiler will reject programs with invalid types.
- Strongly typed: the type system cannot be overriden, and most type operations are explicit.
Safe
Nara has the following caracteristics in favor of safety:
- Garbage Collection: All dynamic memory is managed by a garbage collector, freeing the programmer from errors like leaks, double free, etc.
- No pointers: Prevents having invalid pointers to data, and reduces attack vectors.
- Nullability: Operations that involve
nullare explicit and must be marked. There's syntax like?Typeand others to make it easier. - Errors as values: Errors are explicit and must be managed. However, the language provides syntax sugar for ease of use.
- Strong types: Since all data has defined types, we can't fail on invalid shape of data.
Modern
Bit by bit, new languages introduce new syntax and semantics for better developer experience. Nara has the basics, plus:
- Sum types (unions):
type PaymentMethod = union {
Check(String),
Card(number: String, cvv: u64),
Cash,
}
- Errors as values: Operations with a recoverable error explictly define them, and callers must handle them.
type EditUserError = error {
NotFound,
DbError(reason: String),
ConcurrencyError,
}
pub fun edit_user(data: EditData) EditUserError!void {
// ...
val user = try db.Users.get_by_id(data.id) orelse {
return EditUserError.NotFound
}
// ...
}
Nara has syntax such as try, catch, orelse that makes it easy to handle errors.
- Nullability: Values that can be
nullare explicitly marked, and can be easily worked with. Although the language could support aOption[T]type, we have dedicated syntax for nullable values, and we usenullas usual.
val maybe_user: ?User = try db.Users.get_by_id(data.id)
if (maybe_user) |user| {
// do things with user, which is not null here
user.name = ...
user.age = ...
}
This runs code when there's a value. The opposite can also be done, run code when there's no value:
val maybe_user: ?User = try db.Users.get_by_id(data.id)
maybe_user orelse {
// This runs when `maybe_user` is null
// ...
}
- Pattern matching: