Pronounced "engine".
An expressive and easy to use high-level programming language.
Make it work - extremely early development.
Your entrypoint file must define a main() function. It's found and run automatically. Most of your code will live inside of this function, but not everything.
| example | scope | value |
|---|---|---|
var z = "world" |
local | mutable |
const status = "go" |
local | immutable |
static DATA = [1, 2, 3, 4, 5] |
global | immutable |
Defines a variable who's value can be changed.
var x = "hello"
x = "goodbye" âś…
Defines a constant who's value cannot be changed.
const x = "hello"
x = "goodbye" ❌ // value is immutable
Used for global declarations, which can only exist at the top-level of a file, not inside functions.
- usually inlined at compile time
- strings not inlined if longer than 32 bytes
- arrays and tuples not inlined if size is greater than 4 items or if any item is not a primitive type
static VERSION = "v3" // inlined at compile time
static DATA = [1, 2, 3, 4, 5] // not inlined
fn main() {
print(VERSION)
print(DATA)
}
stringi64,i32,i16,i8,u64,u32,u16,u8,f64,f32boolarray<type>voidmap<key_type, value_type>set<value_type>channel<type>fn<...paramN, return_type>
const thing: string = "one"
var answer: u64 = 42
var truth: bool = false
const things: array<i64> = [1, 2, 3]
const stuff: array<string> = ["shirt", "hat", "coat"]
fn sideEffects(): void {
// do something
}
Supported for literals and expressions, as well as inside functions (requires explict types for fn params and return).
const thing = "one" // inferred as `string`
const answer = 42 // inferred as `i64`
const pi = 3.14 // inferred as `f64`
const result = 3 + 2 // inferred as `i64`
Log to the console, without formatting.
const name = "ngn"
echo(name)
// ngn
echo("Hello")
echo("World")
// HelloWorld
Line logging to the console. Implicit \n.
print("Hello")
print("World")
// Hello
// World
const x = 5
print("x plus 1 is ${x + 1}") // x plus 1 is 6
const greeting = "world"
print("Hello, ${greeting}!") // Hello, world!
// you can escape if needed
print("hello \${x}") // hello ${x}
You can parse a JSON string or an array.
const data = json.parse('{"name": "ngn"}')
print(data.name) // ngn
You can stringify an object or an array.
const data = { name: "ngn" }
const str = json.stringify(data)
print(str) // {"name": "ngn"}
Return the length of a string.
Search a string for a given pattern, and return the index number of the first instance found. If no pattern is found, returns -1. You can pass an optional start index.
const sent = "I learned to draw today."
const ind = sent.index("to") // 10
Determine if a string includes a given pattern. Returns a bool.
const weather = "sunny"
const inc = weather.includes("sun") // true
Determine if a string starts with a given pattern. Returns a bool.
var process = "complete"
const beg = process.starts("c") // true
Determine if a string ends with a given pattern. Returns a bool.
var process = "working"
const end = process.ends("ing") // true
Create an array of strings by splitting on a pattern of characters within a string. If you do not pass a pattern, each character in the string is split individually. Preserves the original string.
const sent = "What. On. Earth."
const split_sent = sent.split(".") // ["What", " On", " Earth", ""]
var greeting = "Hello"
const split_greeting = greeting.split() // ["H", "e", "l", "l", "o"]
Replace a pattern with a string. search can be a string or a RegEx; but if a string is passed, only the first occurrence is replaced. Preserves the original string and returns a new one.
var plain = "Forge ahead"
const fancy = plain.replace("a", "@") // "Forge @head"
var plain = "Forge ahead"
const fancy = plain.replace(/a/g, "@") // "Forge @he@d"
Copies an entire string or a section of it, based on indices. This does not change the string you copied from, but returns the copied value as a new string.
- If
startis provided butstopis not, it copies everything upto and including the end of the string. - If
stopis provided (impliesstart), the copy excludes the item at that index. - If neither is provided, the entire string is copied.
const some = "Some Stuff"
const copied = some.copy(5)
print(copied) // "Stuff"
print(some) // "Some Stuff"
var all = some.copy()
print(all) // "Some Stuff"
print(some) // "Some Stuff"
Remove a section of a string by providing a start index and an optional stop index. This changes the original string and returns the sliced section as a new string.
- If
stopis provided, the slice excludes the item at that index. - If
stopis not provided, it removes everything upto and including the last item. - Since you're mutating the original string, it must be declared with
var.
var quote = "I flew too close to the sun on wings of pastrami."
const sliced = quote.slice(24, 31)
print(orig) // I flew too close to the wings of pastrami.
print(sliced) // "sun on "
Transform a string to all uppercase, returning a new string. Preserves original string.
const version = "one"
print(version.upper()) // ONE
Transform a string to all lowercase, returning a new string. Preserves original string.
var version = "ONE"
print(version.lower()) // one
Remove whitespace from both ends of a string, returning a new string. Preserves original string.
var = thing = " strong "
print(thing.trim()) // "strong"
Repeat a string some number of times.
const ending = "goodbye"
print(greeting.repeat(2)) // goodbyegoodbye
There are currently no number methods, but we do have a math mod (see below), or you can use the extend keyword to add your own (see below).
If you want to mutate arrays, be sure to declare them with var
var stuff = ["hat", "coat", "gloves"]
const ages = [3, 8, 15, 23]
const mixed = ["hat", true, 7] ❌ // cannot mix types
Return the size of the array.
Push, i.e. add, an item into an array. By default, it pushes at the end. To push into another location, provide the index number. Returns the new size of the array as an i64.
var stuff = ["guitar", "shirt"]
const size = stuff.push("hat")
print(size) // 3
print(stuff) // ["guitar", "shirt", "hat"]
stuff.push("coat", 0)
print(stuff) // ["coat", "guitar", "shirt", "hat"]
Pull, i.e. remove, an item from an array. By default, it removes from the end. To pull from another location, provide the index number. Returns the removed item's value.
var stuff = ["coat", "guitar", "shirt", "hat"]
const pulled = stuff.pull()
print(pulled) // hat
print(stuff) // ["coat", "guitar", "shirt"]
const pulled_one = stuff.pull(1)
print(pulled_one) // ["guitar"]
print stuff // ["coat", "shirt"]
Copies an entire array or a section of it, based on indices. This does not change the array you copied from, but returns the copied items as a new array.
- If
startis provided butstopis not, it copies everything upto and including the last item. - If
stopis provided (impliesstart), the copy excludes the item at that index. - If neither is provided, the entire array is copied.
const stuff = [10, 20, 30, 40, 50]
const copied = stuff.copy(3)
print(copied) // [40, 50]
print(stuff) // [10, 20, 30, 40, 50]
var all = stuff.copy()
print(all) // [10, 20, 30, 40, 50]
print(stuff) // [10, 20, 30, 40, 50]
Remove a section of the array by providing a start index and an optional stop index. This changes the array and returns the item(s) as a new array.
- If
stopis provided, the slice excludes the item at that index. - If
stopis not provided, it removes everything upto and including the last item.
var stuff = [10, 20, 30, 40, 50]
const sliced = stuff.slice(1, 3)
print(sliced) = [20, 30]
print(stuff) // [10, 40, 50]
Add multiple items to an array; optionally, at a specific index. Returns the new size of the array.
- If
startis not provided, it adds the items at the end.
var stuff = [10, 20, 30]
stuff.splice([40, 50]) // [10, 20, 30, 40, 50]
const size = stuff.splice([45, 47], 4)
print(stuff) // [10, 20, 30, 40, 45, 47, 50]
print(size) // 7
For each item in an array, execute a closure.
var things = ["hat", "gloves", "coat"]
things.each(|t, i| {
print("{i}: {t}")
})
Similar to arrays, but can contain mixed types. However, they are fixed-size and immutable.
const point = (10, 20)
// they can be indexed like arrays
const x = point[0] // 10
const y = point[1] // 20
const tup = (10, "hello", true)
Return the size of the tuple.
Check if a tuple contains a specific item.
const tup = (10, "hello", true)
const has_hello = tup.includes("hello")
print(has_hello) // true
Search a tuple for a given item, and return the index number of the first instance found. If no item is found, returns -1.
const tup = (10, "hello", true)
const ind = tup.index("hello") // 1
Copies an entire tuple or a section of it, based on indices. This does not change the tuple you copied from, but returns the copied items as a new tuple.
- If
startis provided butstopis not, it copies everything upto and including the last item. - If
stopis provided (impliesstart), the copy excludes the item at that index. - If neither is provided, the entire tuple is copied.
const tup = (10, "hello", true)
const copied = tup.copy(1)
print(copied) // ("hello", true)
print(tup) // (10, "hello", true)
Convert a tuple to an array. Items must be of the same type.
const tup = (10, 20, 30)
const arr = tup.toArray()
print(arr) // [10, 20, 30]
Join a tuple into a string, separated by a given delimiter.
const tup = (10, 20, 30)
const joined = tup.join(",")
print(joined) // "10,20,30"
You can create raw objects using the {} syntax and access their properties using the dot notation.
const person = {
name: "John",
age: 30,
isStudent: false
}
print(person.name) // John
print(person.age) // 30
print(person.isStudent) // false
You can also use shorthand syntax for assigning values to object fields.
const name = "John"
const age = 30
const isStudent = false
const person = { name, age, isStudent }
print(person.name) // John
print(person.age) // 30
print(person.isStudent) // false
If you want to add methods to built-in types, you can use the extend keyword. This feature applies to following types:
- number (generic that applies to all numeric types)
- f64, i32, u8, etc (for specific numeric types)
- string
- bool
- array
- tuple
- map
- set
extend array {
fn isEmpty(): bool {
return this.size() == 0
}
}
extend number {
fn isEven(): bool {
return this % 2 == 0
}
fn double(): number {
return this * 2
}
}
extend string {
fn isBlank(): bool {
return this.trim().length() == 0
}
}
fn main() {
[1, 2, 3].isEmpty() // false
// if using a number directly, wrap in parenthesis
(2).isEven() // true
const x = 2
x.isEven() // true
// if a number method returns the generic `number` type, you should explicitly set the result type
const y: i32 = x.double()
" ".isBlank() // true
}
ngn provides two built-in enums for common patterns: Result and Maybe
Result represents an operation that can either succeed or fail.
Ok(T)— The operation succeeded with a value of typeTError(E)— The operation failed with an error of typeE
fn divide(a: i64, b: i64): Result<i64, string> {
if b == 0 return Error("Division by zero not allowed")
return Ok(a / b)
}
fn main() {
const result = divide(10, 2)
match (result) {
Ok(value) => print("Ok: {value}"), // Ok: 5
Error(msg) => print("Error: {msg}"), // Error: Division by zero not allowed
}
}
Maybe represents a value that may or may not exist.
Value(T)— The value existsNull— The value does not exist
fn findUser(id: u64): Maybe<string> {
if (id == 1) return Value("Jason")
if (id == 2) return Value("Brad")
return Null
}
const user1 = findUser(1)
const user2 = findUser(99)
match (user1) {
Value(name) => print("Found: {name}"), // matches
Null => print("User not found"),
}
match (user2) {
Value(name) => print("Found: {name}"),
Null => print("User not found"), // matches
}
check is a way of guarding logic that requires a value. It can only be used for variables of type Maybe<T> or Result<T, E>.
- If it evaluates to
Null, the statement block is run and it must eitherreturnorbreak. - If it evaluates to a value, the declared variable (
uin the example) is assigned the value and the statement block is skipped.
fn getUser(user?: string): Result<User, string> {
check var u = user {
// failure case
return Error("User not found")
}
// success case
return Ok(u)
}
const user = getUser("jason")
match (user) {
Ok(user) => print("User: ${user}"),
Error(msg) => print("Error: ${msg}"),
}
You can define your own enums for domain-specific types.
enum Color { Red, Green, Blue }
enum Status {
Active,
Inactive(string) // With associated data
}
fn main() {
const color = Red
print(color) // Color::Red
const status = Inactive("maintenance")
print(status) // Status::Inactive (maintenance)
match (status) {
Active => print("Status: Active!"),
Inactive(value) => print("Status: Inactive with reason, {value}")
}
}
Run the statement block indefinitely. Use break to exit the loop.
loop {
statement
statement
}
Run the statement block while the condition is true. Not guaranteed to run at all.
while (condition) {
statement
statement
}
Can be inlined if only using a single statement.
while (condition) statement
To always run the statement block once, before checking the condition.
while once (condition) {
statement
}
Run a statement block for each message in a channel or items in a collection.
Arrays have an
eachmethod, so you don't need to use a for loop with them unless you want to.
for (msg in <-? channel) {
print(msg)
}
for (item in items) {
print(item)
}
Run a statement based on if a condition is true.
For blocks with only a single statement, you can use the following syntax:
if (condtion) statement : (condition) statement : statement
if (condition)
statement
: (condition)
statement
: statement
The below syntax is required if any of your blocks have multiple statements. Note the first brace comes directly after the if keyword.
if {
(condition)
statement
statement
: (condition)
statement
:
statement
}
Match a value against one test case; optionally, provide a default case.
If a match is found:
- that branch's statement block is run.
- other cases are skipped, including the default, unless a matched statement block contains the
nextkeyword.nextwill only try to match the very next case.
const value = 3
match (value) {
1 => statement,
2 | 3 => statement, // matches 2 or 3
4 => {
statement
statement
next
}, // matches 4, runs both statements, then tries to match the next case
_ => statement // matches any other value
}
Functions create an isolated environment, meaning it can't access values outside of itself. If you need access to a value outside the environment, pass it as a parameter; but there are exceptions, which you can always access:
- globals (imports, statics, models, enums, functions)
- sibling functions
If passing a function as a param, you can mark the param like fn<param1_type, param2_type, paramN_type, return_type>. return_type is always last in the list, even if that means it's the only type listed.
Function params must be explicitly typed - otherwise ngn will show a console warning.
fn add(a: i64, b: i64): i64 {
return a + b
}
fn add(a: i64, b: i64): i64 {
return (
a +
b
)
}
fn add(a: i64, b: i64): i64 a + b
Functions that only perform side-effects don't need a return type, but you can declare void if you want.
fn doThing() {
print("something")
}
When you mark a function param as owned, here is what happens:
- the value is mutable within the function, if it was declared with
var - ownership of the passed data is moved to the function
- the var or const is no longer accessible outside of the function
- ngn will cleanup any associated memory after the function finishes
var x = "hello"
// the `<` means it requires an owned string
fn createRockMusic(stuff: <string) {
// do stuff: read and/or mutate
}
createRockMusic(x) âś… // moves ownership of `x` to the function
print(x) ❌ // `x` is no longer available, since it's ownership was moved
This is the default for all params.
var x = "hello"
fn readThing(thing: string) {
// do thing: but cannot mutate
}
readThing(x) âś… // does not move ownership of `x` to the function
print(x) âś… // `x` is still available
In this example, suffix is optional. Inside the function, it is either Maybe::Value<T> or Maybe::Null. There are a couple of ways to handle checking which variant it is:
fn greet(name: string, suffix?: string): string {
// explicit match
match (suffix) {
Value(s) => return "Hello ${name}${s}"
Null => return "Hello ${name}"
}
}
print(greet("Bob")) // "Hello Bob"
print(greet("Bob", "!")) // "Hello Bob!"
fn greet(name: string, suffix?: string): string {
// Check if a value can be unwrapped from the enum variant. If so, assign to local variable and run the statement block; otherwise, it's `Maybe::Null`.
if (let s = suffix) {
return "Hello ${name}${s}"
}
return "Hello ${name}"
}
print(greet("Bob")) // "Hello Bob"
print(greet("Bob", "!")) // "Hello Bob!"
Default params are implicitly optional.
fn greet(name: string, suffix: string = "!") {
print("Hello ${name}${suffix}")
}
print(greet("Bob")) // "Hello Bob!"
print(greet("Bob", ",")) // "Hello Bob,"
Create a key, value map. Type declartion is required.
const m = map<i64, string>()
// add an entry
m.set(1, "one") // returns the map
// chain set
m.set(2, "two").set(3, "three")
// checks if an entry exists, based on key
m.has(1) // returns a bool
// get an entry
m.get(1) // returns the value, or void if not found
// remove an entry
m.remove(1) // returns the removed value
// get the size
m.size() // returns the number of entries in the map
Create a set of values.
- Type declartion is required
- Values are deduplicated
const s = set<string>()
// add a value
s.add("one") // returns the set
// chain add
s.add("two").add("three")
// checks if a value exists
s.has("one') // returns a bool
// remove a value
s.remove("one") // returns a bool of whether the value was removed
// get the size
s.size() // returns the number of values in the set
Closures are similar to functions, but have important differences:
- assign them with
constthen call like a function - access to external values, even ones outside its environment
- uses pipe syntax to wrap params
- param ownership transfer is the same as functions
- to mutate the value of a variable from within a closure, use
state()to declare the variable.
-
The closure captures outside values at creation.
var count = 0 const incrementBy = |a: i64| count + a // captures `count` at 0 print(incrementBy(10)) // 10 print(incrementBy(5)) // 5 count = 100 print(incrementBy(7)) // still 7 const incrementCount = |a: i64| count + a // captures `count` at 100 print(incrementCount(7)) // 107 -
You can mimic classic "close over" behavior by returning a closure from a function.
fn main() { var count = 10 fn adder(c) { return |m| { return c + m } } const add_it = adder(count) // add_it becomes the returned closure from adder, // and the value of `c` is locked-in as 10 // since that was the value of `count` when it was passed print(add_it(3)) // 13 count = add_it(5) // sets count to 15 const add_me = adder(count) // param `c` is 15 for this closure print(add_me(5)) // 20 }Or, the closed over value can be within the function. In this case, we use
state()to declare the variable since we need to mutate it from within the closure.fn main() { fn make_counter() { var count = state(0) return || { count.update(|c| c + 1) print(count) } } const counter = make_counter() counter() // 1 counter() // 2 counter() // 3 }
You can create typed objects using models, then create a new instance of a model.
Create object structures.
model Dog {
name: string,
breed: string
}
You can extend a model's functionality with groups of methods via roles. Declare one or more method signatures and/or method implementations. Use this to group methods into roles in order to define their functionality for models.
role Animal {
fn speak(): void
}
Extend a model's functionality with methods. You can implement custom methods, apply one or more roles, or a mix of both.
// extend with custom methods
extend Dog {
fn fetch(thing: string): bool {
return attemptToFetch(thing)
}
}
// extend with role
extend Dog with Animal {
fn speak(): void {
print("Woof, woof!")
}
}
Now, putting it all together:
const my_dog = Dog {
name: "Apollo",
breed: "Labrador"
}
print(my_dog) // { name: Apollo, breed: Labrador }
print(my_dog.name) // Apollo
const fetched = my_dog.fetch("stick")
print(fetched) // either true or false
my_dog.speak() // Woof, woof!
You may also choose to create a constructor method and use it to create a new instance of a model.
model User {
name: string,
age: u32
}
extend User {
fn new(name: string, age: u32): User {
return User { name, age }
}
}
fn main() {
var user = User.new("Chloe", 27)
}
When you create an instance of a model, it's essentially an object - although it can have methods attached to it as well.
The general rule is that you can mutate based on how the variable was declared (var, const). However, you can't change a field's type.
Here are the ways to manipulate an object's fields, based on the above example code:
- direct assignment:
user.age = 7 - entire object:
user = { name: "Ben", age: 56 } - method:
user.changeName("Stacie") - by
const,staticvariables: ❌ not allowed, as these are all strictly immutable
There's no need to fear this in ngn. It's an implicit reference to the instance that a method is called on.
For models, it gives you access to the instance's fields and other methods.
model User {
name: string,
age: u32
}
extend User {
fn greet(): string {
print("Hello, I'm {this.name}")
}
fn changeName(name: string): void {
this.name = name
}
}
var user = User { name: "Jason", age: 47 }
user.greet() // "Hello, I'm Jason"
For custom type methods, it gives you access to the type's value.
extend string {
fn isBlank(): bool {
return this.trim().length() == 0
}
}
const name = ""
print(name.isBlank()) // true
Send and receive data.
- You must declare a channel with
const - You must provide a data type for the channel's messages.
Use <- to both send and receive messages. Let's assume we have a channel called line.
line <- "hello"would send a string message to the channel.<- linewould cause the program to stop and wait for a single message.
Regarding the last point: if you had multiple things sending messages, you have the following options:
// Would wait on two messages before continuing the program.
<- line
<- line
// If you know how many messages to wait on, here's a shorthand version of the above:
<-2 line
// You can even base it off of other things:
<-tasks.size() line // array size
<-(x + y) line
// If you don't know how many messages you'll receive.
// You'll need to close the channel for this to work (example futher below).
<-? line
fn main() {
const c = channel(): string
// Send a message
c <- "first"
// Optionally close the channel for this example
c.close()
// Assign channel output to a variable
// Receiving "first" will still work here, because of buffering
const msg = <- c
print("Received: ${msg}")
// This will fail because the channel is closed and empty.
const fail = <- c
}
You can send a closure to a channel:
fn main() {
const job_queue = channel(): fn<i64, void>
// (See next section for details on threads)
const done = thread(|| {
print("Worker started")
loop {
match (<-? job_queue) {
Value(task) => task(42),
Null => break
}
}
print("Worker finished")
return
})
job_queue <- |n: i64| { print("Task A executing with ${n}") }
job_queue <- |n: i64| {
const res = n * 2
print("Task B executing: ${n} * 2 = ${res}")
}
// must close the channel to break out of `while` loop
job_queue.close()
print("Jobs sent")
// wait for jobs to complete
<- done
print("Jobs complete")
}
Jobs sent
Worker started
Task A executing with 42
Task B executing: 42 * 2 = 84
Worker finished
Jobs completeChannels can even contain other channels, and you can send/receive data within those inner channels.
fn main() {
const request_line = channel(): channel<string>
thread(|| {
// Thread waits for inbound data on the request_line channel,
// which happens to be another channel that we assign to a constant.
const reply_channel = <- request_line
// Reply back on the private channel
reply_channel <- "Your order is ready!"
})
// Create a private response channel
const private_channel = channel(): string
// Send private channel, which the worker is waiting for
request_line <- private_channel
// Wait for the private reply
print(<- private_channel)
}
Allows you to do work while, optionally, continuing to do work in the main thread. Threads take a closure with no parameters, but have access to their surrounding scope. They also return a channel, if that fits your usecase.
Standalone threads are risky because as soon as the main program ends, all unfinished threads are killed. In the below example, setData(value) may never finish.
fn main() {
const value = 100
thread(|| {
setData(value)
})
// Continue doing other work while the thread runs
print(value)
}
In such cases, use the returned channel to await the thread.
fn main() {
const value = 100
// "done" is a channel we can use for signaling
const done = thread(|| {
setData(value)
return // returning from a thread sends that data to the created channel
})
// Continue doing other work while the thread runs
// Now wait until we receive a message,
// indicating thread work is done
<- done
}
Threads may run in parallel or sequentially but unordered; however, you can control the order in which you wait on their results.
fn main() {
print("1. Main started")
// Create a thread, to do some async work
const c = thread(|| {
print(" 2c. Thread c started (sleeping...)")
sleep(2000)
print(" 3c. Thread c sending message")
return Ok("Hello from channel c!")
})
// Create a thread, to do some async work
const d = thread(|| {
print(" 2d. Thread d started (sleeping...)")
sleep(2000)
print(" 3d. Thread d sending message")
return Error("Oh this is bad channel d!")
})
print("4. Main doing other work while thread runs...")
// This should block until the "c" thread sends a message
const msgc = <- c
print("5c. Main received, from thread c: ${msgc}")
// This should block until the "d" thread sends a message
const msgd = <- d
print("5d. Main received, from thread d: ${msgd}")
}
1. Main started
4. Main doing other work while thread runs...
2c. Thread c started (sleeping...)
3c. Thread c sending message
2d. Thread d started (sleeping...)
3d. Thread d sending message
5c. Main received, from thread c: Result::Ok (Hello from channel c!)
5d. Main received, from thread d: Result::Error (Oh this is bad channel d!)If you're unsure how much data is coming, use a for loop, and then close the channel at the end of the input in order to indicate that no more messages can be sent. Below, we're simulating "unknown" amounts of data.
fn main() {
// In this example, we can't use the thread's returned channel,
// because we need to close the channel from within the thread
// in order to signal the `for` loop to stop.
const c = channel(): string
thread(|| {
c <- "A"
c <- "B"
c <- "C"
c.close()
})
for (msg in <-? c) {
print("Got: {msg}")
}
print("Done")
}
It's safe to sequentially mutate shared data outside of threads or within a single thread. However, if one or more threads might mutate data, use state() to declare the variable. This gives you safe, atomic operations for mutating the data by using state variable methods.
You'd also use state() if you need to mutate data from within a closure.
fn main() {
var counter = state(0)
const done = channel(): bool
thread(|| {
// Pass a closure that mutates the data.
// The closure receives the current value of `counter` via a param.
counter.update(|n| n + 10) // implicit return used
print("added 10")
done <- true
})
thread(|| {
counter.update(|n| n + 5)
print("added 5")
done <- true
})
// If number of awaited messages is known, you can declare that here.
// They'll be returned as an array, if you need to assign them.
<-2 done
print(counter) // Always 15
}
If needed, you also have access to these variable methods when using state():
.read(), gets the current value.write(), sets the current value - which replaces the existing one. Be careful, as it can be tricky to ensure proper mutation order when coupled with.update().
If you have multiple tasks, here is one way you can spawn each of them in their own thread.
fn main() {
const done = channel(): bool
var results = state([])
fn task1(): string {
print("Doing task 1")
return "Task 1 done"
}
fn task2(): string {
print("Doing task 2")
return "Task 2 done"
}
fn task3():string {
print("Doing task 3")
return "Task 3 done"
}
const tasks = [task1, task2, task3]
// Spawn threads
// Optional index value as the second closure param
for (task in tasks) {
thread(|| {
const result = task()
results.update(|r| {
r.push(result)
return r
})
done <- true
})
}
<-tasks.size() done
print("Spawned results: ${results}")
}
Use fetch to make HTTP requests, such as to external APIs. It returns a channel, so you await it with the <- operator.
const response = <- fetch("https://example.com")
print(response)
const response = <- fetch("https://example.com", {
method: "POST",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
},
body: json.stringify({
"name": "John Doe",
"email": "john.doe@example.com",
}),
timeout: 30000,
})
print(response)
url: The URL of the requestoptions: An object containing the options for the requestmethod: The HTTP method of the request - defaults to GETheaders: The headers of the requestbody: The body of the requesttimeout: The timeout of the request, in milliseconds - defaults to 10 seconds
You can handle Request objects.
fn handler(req: Request) {
const path = req.path
const method = req.method
const headers = req.headers
const body = req.body
return Response {
body: "Hello, world!",
}
}
export default {
fetch: handler
}
method: The HTTP method of the requesturl: The URL of the requestprotocol: Whether HTTP or HTTPs was usedhost: The host the client used to make the requestpath: The path of the requestquery: The query string of the requestparams: Query parameters as aMap<string, string>headers: The headers of the requestbody: The body of the requestip: The client's IP addresscookies: The cookies sent with the request
clone(): Creates a newRequestobject with the same propertiestext(): Parses the body as a string, returns astringjson(): Parses the body as JSON, returns a Result enumformData(): Parses URL-encoded body, returns aMap<string, string>
You can create a Response object to send HTTP responses.
fn handler(req: Request) {
return Response {
body: "Hello, world!",
}
}
export default {
fetch: handler
}
status: The HTTP status code - default is 200statusText: The HTTP status text - default is ""ok: A boolean indicating whether the response status code is in the range 200-299headers: The headers to include in the responsebody: The body of the response - default is ""
text(): Parses the body as a string, returns astringjson(): Parses the body as JSON, returns a Result enumformData(): Parses URL-encoded body, returns aMap<string, string>
You can use export and import to create modules in your project. This is a functions-only feature.
// math.ngn
export fn add(a, b) a + b
export fn subtract(a, b) a - b
Here, we look for an add function from a math.ngn file within the same directory that your main.ngn file is in.
// main.ngn
import { add } from "math.ngn"
fn main() {
print(add(21, 3)) // 24
}
import { add } from "./lib/math.ngn"
import { add as adder } from "math.ngn"
// main.ngn
import * as Math from "math.ngn"
print(Math.subtract(10, 2)) // 5
// math.ngn
fn add(a, b) a + b
export default add
// main.ngn
import add from "math.ngn"
We call this the toolbox.
You can import in different ways:
import { abs } from "tbx::math"; use functions directly (best for tree-shaking)import { abs as ABS } from "tbx::math"; aliased functionsimport Math from "tbx::math"; use functions via aliasMath.absimport * as Math from "tbx::math"; (use same as above)
abs: return the absolute value of a number.abs(-5) // 5ceil: return the smallest integer greater than or equal toceil(3.2) // 4floor: return the smallest integer less than or equal tofloor(3.9) // 3round: return the number rounded to the nearest integerround(4.5) // 5sin: (Trigonometry? If you know, you know.)sin(0) // 0
assert: assert that a condition is true
import { assert } from "tbx::test"
fn main() {
assert(1 + 1 == 2)
}
Create an HTTP server.
serve: create an HTTP server
import { serve } from "tbx::http"
fn handleRequest(req: Request): Response {
return Response {
status: 200,
headers: map<string, string>(),
body: "Hello from ngn HTTP server! Path: ${req.path}"
}
}
fn main() {
print("Starting HTTP server on port 3000...")
serve(3000, handleRequest)
}
- default export with fetch method (serve called under the hood)
fn fetch(req: Request): Response {
return Response {
status: 200,
body: "Hello from export-based API!",
headers: map<string, string>()
}
}
export default { fetch: fetch }