Solace/README.md

250 lines
6.9 KiB
Markdown

# Solace
A language to find solace after using JavaScript
## Proposal
A strongly typed programming language that compiles to JavaScript and comes with the following features:
* Error unions: `Error!string`
* Optional values: `?string`
* scoped blocks
* exhaustive pattern matching
* "unreachable" as keyword
* defer statements
* built-in reactivity system (using signals?)
## Syntax
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 of classes, Solace uses scopes that work just like JavaScript's scopes.
It also looks very similar to TypeScript, if you want:
```solace
fn sum(a: number, b?: number): number {
if b is undefined {
return a
}
return a + b
}
```
// But also supports Optionals on a syntax level:
```solace
fn sum(a: number, b: ?number): number {
if b is none { // here b has to be defined, but can be set to none
return a
}
return a + b
}
```
// And Error unions:
```solace
fn sum(a: number, b?: number): !number {
if b is undefined {
return failure("Second number is missing")
}
return success(a + b)
}
```
Error unions default to Error, so the last example is equivalent to this one:
```solace
fn sum(a: number, b?: number): Error!number {
if b is undefined {
return failure("Second number is missing", Error)
}
return a + b
}
```
A more specific error might be needed, though. Although Solace doesn't really
use classes, it can handle inheritance:
```solace
// The following lines compile to:
// function NumberError(message) {
// this.name = "NumberError"
// this.message = message
// }
// NumberError.prototype = Error
fn NumberError(message: string) extends Error {
_.name = "NumberError"
_.message = message
}
fn sum(a: number, b?: number): NumberError!number {
if b is undefined {
return failure("Second number is missing", Error)
}
return a + b
}
```
Solace also supports initialized default return values, similar to swifts inout parameters.
They are really useful for short functions:
```solace
fn fizzbuzz(until: number) => (output: string[] = []) {
// var output = [] // no need, because it is already defined
for i in 1..=until { // this is an inclusive range
// 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:
```solace
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 i..=until { // an exclusive range would be i..<until
output.push(numberToFizzBuzz(i))
}
}
```
If can be used as a statement or expression:
```solace
fn twentyIfTrue(x: boolean): ?number {
const value = if x {
20
} else {
none
}
return value
}
```
Of course this could be written with less lines. Here with a named return variable.
```solace
fn twentyIfTrue(x: boolean) => (value: ?number) {
value = if x { 20 } else { none }
// or
value = x ? 20 : none
// or
value = if x: 20
else: none
}
```
An if expression always needs to cover all cases to ensure that the variable
will be assigned. If statements don't have that requirement, but then the
return value needs to be initialized:
```solace
fn twentyIfTrue(x: boolean) => (value: ?number = none) {
if x: value = 20
}
```
Now to the more interesting part: Reactivity and unified handling of values.
```solace
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")
// This defer statements runs after the function finished, but only if the
// given pattern matches, in this case if ok(fd) returns a value.
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
}
// This defer statement always runs after everything else finished.
// You can see it like try-finally, for functions.
defer {
console.log("This message will show after the function finished.")
}
// 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 an Error
// union essentially, while err(x) returns the error or null
ok(fd) -> handleFile(_)
err(fd) -> handleError(_)
_ -> continue // matches need to handle all possible cases
}
}
}
```
Here is another way to write the former example:
```solace
import fs from "node:fs/promises"
fn openFile(path: string) {
live fd = fs.open(path, "rw")
// this defer executes only if at the end of the function ok(fd) returns a value
defer when ok(fd) |file| {
console.log("File closed.")
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 an Error
// union essentially, while err(x) returns the error or null
ok(fd) -> handleFile(_)
err(fd) -> handleError(_)
_ -> continue // matches need to handle all possible cases
}
}
}
```
Here some more loop constructs:
```solace
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 stop when i reaches 22. Currently it has the value", i++)
}
```