Learn by writing code
Contents · Previous: Setup and first run · Reference: Syntax
Learn High (.nagi), the application language, in this order: variables → functions → lists → classes → failures. Follow Setup and first run, then save the code in your own working folder. The same nagic command works on Windows, Linux, and macOS.
On this page
1. Values and types
Place this fragment inside a function such as main:
count = 10 # i64, inferred from the value
rate = 1.5 # f64
enabled = True # bool: use True / False
name = "Nagi" # str: Unicode text is supported
age: i32 = 18 # An annotation: name: type = value
count += 1
print(count)
You can reassign variables but cannot change their types. count = "ten" is a type error. Conditions require bool; you cannot use an integer as a condition with if count:.
Different integer types do not mix implicitly. i64(age) widens i32 to i64. The reverse, i32(count), can exceed the range and returns Result[i32, Error]. Failure handling comes later in this guide.
2. Define functions
def add(a: i64, b: i64) -> i64:
return a + b
def main():
print(add(20, 22))
Save and run this complete program to print 42. Parameters use name: type; return types use -> type. An omitted return type means unit, with no returned value. main is the entry point.
3. Lists, classes, branches, and loops
A list literal is [1, 2, 3], and its type annotation is List[i64]. append(values, 4) adds an element. A class groups named fields into a type.
Save this complete program as basics.nagi. It is also available as a sample:
class Point:
x: f64
y: f64
def sum_numbers(values: view[i64]) -> i64:
total = 0
for value in values:
total += value
return total
def main():
values = [1, 2, 3]
append(values, 4)
total = sum_numbers(view(values))
print(total)
point = Point(x=3.0, y=4.0)
print(point.x + point.y)
if total >= 10 and len(values) == 4:
print("OK")
else:
print("NG")
for index in range(3):
print(index)
count = 0
while count < 2:
count += 1
print(count)
nagic run basics.nagi
Output, in order: 10, 7, OK, 0, 1, 2, 2.
- Construct a Point with all fields named:
Point(x=..., y=...). PositionalPoint(3.0, 4.0)is not supported. - Read fields with
point.x. Classes do not yet have methods or inheritance. for value in valuesreads elements in order. Currently this supports primitives such as integers, floats, and bool, and classes containing only Copy fields. Copy types can be copied without moving ownership. Iteration overList[str]is not supported.range(3)gives0, 1, 2. It takes one argument and excludes the end.whilerepeats while its condition isTrue.break/continueare not supported.- Combine conditions with
and/or/not. There is noelif; put anotherifinsideelsewhen needed.
view(values) borrows the list for reading instead of giving it away. The next section explains this.
4. Borrow with view when you only need to read
Passing a string or list directly to a user-defined function moves ownership. Using the variable afterward is an error.
# This program is rejected
def take_name(name: str):
print(name)
def main():
name = "Nagi"
take_name(name)
print(name) # name was moved to take_name
A function that only reads can accept view[str]. Call it with view(name). This complete program is in borrowing.nagi:
def name_size(name: view[str]) -> i64:
return len(name)
def main():
name = "Nagi"
print(name_size(view(name)))
print(name_size(view(name)))
duplicate = copy(view(name))
print(duplicate)
print(name)
Output: 4, 4, Nagi, Nagi. copy(view(name)) creates a separate owned string. Request copies explicitly when you need them.
Built-ins such as print and len read their input and do not move a string merely because you pass it. Functions do not all handle arguments in the same way.
len(str) counts UTF-8 bytes rather than characters: len("あ") is 3. Modifying the original data is also restricted while a view is alive. See ownership.
5. Return failures with Result
Result[T, Error] returns T on success or Error on failure. Use ok(value) and error("reason") to construct them.
try operation extracts the success value, or immediately returns the failure to the caller. The function using try must itself return Result.
Save this complete example as input.nagi. It doubles a nonnegative integer. The sample contains the same logic with Japanese prompts.
def double_nonnegative(text: view[str]) -> Result[i64, Error]:
value = try parse_i64(text)
if value < 0:
return error("Enter a nonnegative integer")
return ok(value * 2)
def main() -> Result[unit, Error]:
write("Enter an integer > ")
text = try read_line()
doubled = try double_nonnegative(view(text))
print(doubled)
return ok(print("Done"))
nagic run input.nagi
Enter 21 and press Enter to print 42, followed by the completion message. abc fails number conversion; -1 triggers the error you wrote. Both propagate to main and exit with failure.
write prints without a newline; read_line reads one line. return ok(print("Done")) wraps the unit returned by printing as a success.
Nagi's try differs from Python's try: ... except:. To separate success and failure, for example to return a default, use match. This is a function example:
def number_or(text: view[str], fallback: i64) -> i64:
match parse_i64(text):
case Ok(number):
return number
case Err(_):
return fallback
Write one Ok case and one Err case. Each bound name is available only in its case; use _ for an unused value. Patterns other than Result are not supported. See error handling for a runnable example and Error inspection.
6. Split code into files
Create two files in the same directory.
math.nagi:
def add(a: i64, b: i64) -> i64:
return a + b
imports.nagi, the entry file:
import "math.nagi"
def main():
print(add(20, 22))
nagic run imports.nagi
The complete example prints 42. Import paths are relative to the file containing the import, not your terminal's working directory. Imported math.nagi does not need a main.
This form loads one namespace, so call add(...). Change the import to import "math.nagi" as math to call math.add(...). A from statement can select a class or function. See imports and Rust integration to distinguish same-named definitions or use Rust libraries.
7. Move on to async and APIs
Define functions that wait for timers or I/O with async def, and call them with await.
async def main() -> Result[unit, Error]:
await sleep(10)
print("Waited 10 milliseconds")
return ok(print("Done"))
This is a complete program. sleep returns unit and needs only await. Fallible database calls use forms such as try await db_open(...). await waits for the operation; try propagates a failure in the returned result.
Continue to HTTP and HTML to build an API you can call from a browser. Use the syntax reference for notation and built-in functions for arguments.