Solace/test.solace

127 lines
4.1 KiB
Text

/*
* Solace looks like Zig, but without the low-level complexities and added
* pattern matching. Typical expressions are very much like in TypeScript and
* it supports the same primitive types undefined, string, number, boolean,
* and their array versions, as well as Map and Set. It uses none instead of
* null and objects are called structs. There is no new keyword and there are
* no classes. Instead, Solace uses scopes that work like JavaScript's scopes.
*/
// Optional parameters
fn sum(a: number, b?: number): number {
if b is undefined {
return a
}
return a + b
}
// Optional values
fn sum(a: number, b: ?number): number {
if b is none { // b has to be defined, but can be set to none
return a
}
return a + b
}
// Error Unions
fn sum(a: number, b?: number): Error!number {
if b is undefined {
return failure("Second number is missing", Error)
}
return a + b
}
// Error unions default to Error, so the last example could also be written like
fn sum(a: number, b?: number): !number {
if b is undefined {
return failure("Second number is missing")
}
return a + b
}
// Solace also supports initialized default return values, similar to Swift's inout parameters.
// They are really useful for short functions:
fn fizzbuzz(until: number) => (output: string[] = []) {
// var output = [] // no need, because it is already defined
for i in 0..=until {
// pattern matching
match i {
_ % 3 == 0 && _ % 5 == 0 -> output.push("fizzbuzz")
_ % 3 == 0 -> output.push("fizz")
_ % 5 == 0 -> output.push("buzz")
_ -> output.push(i.toString())
}
}
// no need, because it's the default return value, but can added for clarity
// return output
}
// Matches are expressions, which means, they return a value.
// Also, just for completeness, match doesn't need a value. So here is the last example changed to take both into account:
fn numberToFizzBuzz(n: number): string {
return match {
i % 3 == 0 && i % 5 == 0 -> "fizzbuzz"
i % 3 == 0 -> "fizz"
i % 5 == 0 -> "buzz"
i -> i.toString()
}
}
fn fizzbuzz(until: number) => (output: string[] = []) {
for i in 0..=until: output.push(numberToFizzBuzz(i))
}
// Now to the more interesting parts of Solace: Reactivity and unified handling of values.
import fs from "node:fs/promises"
fn openFile(path: string) {
// fs.open returns a Promise, and while Solace supports them, it doesn't need
// Promises or async/await, because it has live values.
// Promises (and with them return values of async functions) are translated
// to reactive values if used like this:
live fd = fs.open(path, "rw")
// A defer statement runs after everything else finished, successful or not.
// You can see it like try-finally, for functions.
defer when ok(fd) {
// Because it is a nameless assignment, we use _ to
// access the return value. If you need a name, use:
// defer when ok(fd) |file| { /*...*/ }
_.close() // close file descriptor
}
// live variables would be defined like this in TypeScript:
// { state: "pending", value: Promise<T>, error: null } |
// { state: "err", value: null, error: Error } |
// { state: "ok", value: T, error: null }
watch fd {
match fd {
// ok(x) returns the value of Result types like live, which is essentially an Error Union,
// while err(x) returns the error or null
ok(fd) -> handleFile(_) // here _ refers to the return value of ok(fd)
err(fd) -> handleError(_) // here _ refers to the error value returned by err(fd)
_ -> continue // matches need to handle all possible cases, and continue just ignores
}
}
}
// here all the different ways to write loops:
const myArray: number[] = [23, 42]
for x in myArray: console.log(x)
for y,i in myArray: console.log(`Index ${i} has value ${y}!`)
for i in 0..<23 {
console.log("This will output as long as i is smaller than 23")
}
for i in 0..=23 {
console.log("This will output as long as i is smaller or equal than 23")
}
while i <= 23: console.log("This will output forever, because I forgot to change the value of i")
while i <= 23 {
console.log("This will output until i reaches 23. It's value is now", i++)
}