/* * 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, 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++) }