Programming in Neut
Here, we'll see how to write programs in Neut.
Table of Contents
Variables
You can use let to define variables:
define hey() -> unit {
let x = "hello";
let y: int = 100;
let z: float = 3.8;
print("hey");
}
The compiler warns about unused variables (x, y, and z in the example above). You can use the special name _ to suppress those warnings:
define hey() -> unit {
let _ = "hello";
let _: int = 100;
let _: float = 3.8;
print("hey");
}
lets can be nested using {..}:
define hey() -> unit {
let x = {
let y: int = 100;
let z: float = 3.8;
"hello"
};
print(x); // => hello
}
You can use e1; e2 as syntactic sugar for let _: unit = e1; e2:
define hey() -> unit {
print("a");
print("b");
}
// ↓ (desugar)
define hey() -> unit {
let _: unit = print("a");
print("b");
}
You can use e; as syntactic sugar for let _: unit = e; Unit:
define hey() -> unit {
print("hey"); // using a trailing semicolon
}
↓
define hey() -> unit {
let _: unit = print("hey");
Unit
}
Functions
Defining Functions at the Top Level
You can use the statement define to define functions:
// defining an ordinary function
define my-func1(x1: int, x2: bool) -> bool {
x2
}
// defining a recursive function
define my-func2(cond: bool) -> int {
if cond {
1
} else {
my-func2(not(cond)) // `my-func2` is available here
}
}
// a function that returns a function
define my-func3() -> (int, bool) -> bool {
my-func1
}
define can also define a function with implicit parameters (or "generics"):
// The `a` in the angle bracket is the implicit parameter of `id`
define id<a>(x: a) -> a {
x
}
define use-id() -> int {
let x = 10;
id(x) // calling `id` without specifying `a` explicitly
}
You can also write the type of a explicitly:
define id<a: type>(x: a) -> a { // `type` is the type of types
x
}
Defining Functions in the Body of a Function
You can use => to define an anonymous function:
define foo() -> int {
let f =
(x, cond) => {
if cond {
x
} else {
add-int(x, 1)
}
};
f(10, False) // → 11
}
You can also use define in the body of a function to define a recursive function:
define foo() -> unit {
let f =
define print-multiple-hellos(counter: int) -> unit {
if eq-int(counter, 0) {
Unit
} else {
print("hello\n");
print-multiple-hellos(sub-int(counter, 1))
}
};
f(10) // prints "hello" 10 times
}
The compiler reports an error if you rewrite the example so that the variable f is used more than once. This behavior prevents unexpected copying of values. You can satisfy the compiler by defining the variable as !f. We'll cover this topic in more detail on the next page.
Calling Functions
You can call a function f with arguments e1, ..., en by writing f(e1, ..., en):
define my-func(x: int, y: int) -> int {
add-int(x, y)
}
define use-my-func() -> int {
my-func(10, 20)
}
You can also use named arguments to rewrite the above use-my-func as follows:
define use-my-func() -> int {
my-func{
x := 10,
y := 20,
}
}
Many primitive functions (from LLVM) are also available. Please see Primitives for more.
Algebraic Data Types
You can use the statement data to define ADTs:
data my-nat {
| My-Zero
| My-Succ(my-nat)
}
// In Haskell:
// data my-nat
// = My-Zero
// | My-Succ my-nat
//------------
data my-list(a) {
| My-Nil
| My-Cons(a, my-list(a))
}
// In Haskell:
// data my-list a
// = My-Nil
// | My-Cons a (my-list a)
Parameters in constructors can optionally have explicit names:
data config {
| Config(count: int, cond: bool)
}
You might want to write this vertically using a trailing comma:
data config {
| Config(
count: int,
cond: bool,
)
}
Creating ADT Values
You can use constructors just like normal functions to create ADT values:
define make-my-list() -> my-list(int) {
My-Cons(1, My-Cons(2, My-Nil))
}
define make-config() -> config {
Config{
count := 10,
cond := True,
}
}
Using ADT Values
You can use match to destructure ADT values:
define sum(xs: my-list(int)) -> int {
match xs {
| My-Nil =>
0
| My-Cons(y, ys) =>
add-int(y, sum(ys))
}
}
Nested matching is also possible:
define foo(xs: my-list(int)) -> int {
match xs {
| My-Nil =>
0
| My-Cons(y, My-Cons(z, My-Nil)) =>
1
| My-Cons(_, _) =>
2
}
}
The result of match can be bound to a variable:
define yo(xs: my-list(int)) -> int {
let val =
match xs {
| My-Nil =>
0
| My-Cons(_, _) =>
1
};
val
}
Type Aliases
If you want to give a name to an existing type expression rather than define new constructors, you can use alias.
You can use alias to define a type alias:
alias computation(a) {
either(my-error, a)
}
alias status {
either(unit, int)
}
Miscellaneous
nominal
You can use nominal for forward references of top-level items:
nominal {
define is-odd(x: int) -> bool, // ← declaration of `is-odd`
}
define is-even(x: int) -> bool {
if eq-int(x, 0) {
True
} else {
is-odd(sub-int(x, 1)) // ← using the nominal definition of `is-odd`
}
}
// ↓ the real definition of `is-odd`
define is-odd(x: int) -> bool {
if eq-int(x, 0) {
False
} else {
is-even(sub-int(x, 1))
}
}
if
The core library defines bool as follows:
data bool {
| False
| True
}
You can use if when you use this bool:
define yo(cond: bool) -> unit {
if cond {
print("yo!")
} else {
print("yo")
}
}
// ↓ desugar
define yo(cond: bool) -> unit {
match cond {
| True =>
print("yo!")
| False =>
print("yo")
}
}
admit
You can use admit to postpone implementing a function and satisfy the type checker:
define my-complex-function(x: int, y: bool) -> int {
admit
}
assert
You can use assert as follows:
define fact(n: int) -> int {
assert "n must be non-negative" {
ge-int(n, 0)
};
if eq-int(n, 0) {
1
} else {
let next = sub-int(n, 1);
mul-int(n, fact(next))
}
}
The type of assert ".." { .. } is unit.
assert checks if a given condition is satisfied. If the condition is True, it does nothing. Otherwise, it reports that the assertion has failed and exits the program with exit code 1.
If you pass --mode release to neut build, assert does nothing.
Additional Notes
- More syntactic sugar is also available. For more, see Terms.
- If you want to call foreign functions (FFI), see Statements.