Language Design
The name, much like the language, is a work in progress.
Import statements
Import statements import the whole module and assign it to a variable. Specific things from the module cannot be imported, such as JavaScript's import { function } from pkg
or Python's from pkg import function
. Wildcard imports like Python's from pkg import *
are also not allowed.
import math // assigned to variable `math`
float phi = (math.sqrt(5) + 1) / 2
This is beneficial because different modules can export functions with the same name, and it'll always be clear where each function came from, at the cost of typing slightly more.
Comments
Comments start with //
and can be in the middle or end of a line.
// This is a comment
int a = 2 // This is a comment after a statement
Multi-line comments can be written with /* */
syntax, similar to C.
/*
This is a multi-line comment.
You can even nest comments!
/*
Like this!
*/
*/
Doc comments can be written with ///
or /** */
for single- and multi-line comments, respectively:
import math
/// The value of pi
decimal pi = 3.141592653589793
/**
* A function to find the square root of an array of integers
* @param The array of integers to square root
* @returns An array of square roots
*/
fn sqrtAll(int[] nums) -> float[] {
mut float[] rootVals = []
for i in nums {
rootVals[i] = math.sqrt(nums[i])
}
return rootVals
}
These behave the same as normal comments since it's only the //
and /* */
that matter, all the other /
and *
are optional. However, the LSP will only recognise ///
and /** */
as doc comments.
Variable declaration
[type] [name] = [value]
All variables are constant and immutable by default
int a = 1
a = 2 // Error
To make a variable mutable, you can add the mut
keyword
mut int b = 2
b = 3 // Works!
Functions
fn [name]( [type] [arg], [type] [arg], ... ) -> [return type] {
// Arguments are immutable
// Any variables declared inside are scoped to the function
}
Example function:
fn hello(int a, int b) -> string {
a = 2 // Error
mut int b = b // Creates new scoped variable `b`, let's call it `bscope`.
b = 2 // This will edit `bscope`, not the argument value `b`
return "Hello, world!"
}
// `bscope` is not available here
string hello = hello(a, b) // pass in `int a` and `mut int b` from above
print(hello)
Currying
fn add(int a, int b) -> int {
return a + b
}
fn addA = add(1) // Curried function. `addA` now has the `int a` parameter of `add` prefilled, and only takes one parameter `int b`
int c = addA(2) // 3
Overloading
Functions can be overloaded with different implementations of the same function. This is usually helpful when you want to have multiple parameter types, for example. However, because the language is strictly typed, each overload must have its own implementation.
fn sqrt(int n) -> float {
// implementation for int
}
fn sqrt(float n) -> float {
// implementation for float
}
fn sqrt(decimal n) -> float {
// implementation for decimal
}
However, overloads can only differ by their parameters. So if you'd like to create two functions that take the same input and return different outputs, they must have different names.
fn sqrt(int n) -> float {
// returns float
}
// This overload is not allowed as there's no way to know during compile time which function was used.
fn sqrt(int n) -> decimal {
// returns decimal
}
// This is allowed because the function name is different, so it's not an overload.
fn sqrtDec(int n) -> decimal {
// returns decimal
}
Loops
For loops:
for i in 1:20 {
print(i) // Prints 1, 2, 3, ..., 20
}
for j in 1:2:20 { // You can increment/decrement by other values
print(j) // Prints 1, 3, 5, ..., 19
}
While loops:
mut int j = 2
while j > 0 {
j--
}
// `j` is available here since it was declared outside
Conditions
If-else statement:
float pi = 3.14
float euler = 2.71
if pi == 3.14 {
print("Pi moment")
} else if euler == 2.71 {
print("Euler moment")
} else {
print("Not cool")
}
Pattern matching:
fn checkIfAnimal(string animal, string check) -> boolean {
return animal == check
}
string animal = "Dog"
match animal {
"Dog" {
// Equivalent to `if animal == "Dog"`
print("Woof")
}
"Cat" {
print("Meow")
}
"Mouse" {
print("Squeek")
}
checkIfAnimal("Bird") {
// Can call functions that return a boolean value
// The first value is prefilled with the value being checked, `animal` in this case
// Equivalent to `if checkIfAnimal(animal, "Bird")`
print("Chirp")
}
.startsWith("B") {
// Can have methods of the type, for example String.startsWith()
// Equivalent to `if animal.startsWith("B")`
print("Starts with a B")
}
default {
// Required to have a default
print("Not a recognised pet")
}
}
Operators
addition
1 + 2
// Also used for string and list concatenation
substraction
2 - 1
multiplication
3 * 2
division
5 / 2
exponent
2 ** 6
modulo
10 % 4
move bit left
1 << 4
move bit right
6 >> 1
equality
1 == 1 // always strict equality
less than
1 < 2
greater than
1 > 0
less than or equal to
5 <= 6
5 <= 5
greater than or equal to
7 >= 4
7 >= 7
Pipes
Similar to functional programming, we can use a |>
pipe operator to pass values from one function to the next. This will automatically fill in the first value of the receiving function with the value being passed into it. For example, something like this:
fn square(int[] numbers) -> int[] {
mut int[] numbers = numbers
for i in numbers {
numbers[i] = numbers[i] ** 2
}
return numbers
}
fn addToAll(int[] numbers, int adding) -> int[] {
mut int[] numbers = numbers
for i in numbers {
numbers[i] += adding
}
return numbers
}
mut int[] numbers = [1, 2, 3, 4, 5]
numbers = square(numbers) // [1, 4, 9, 16, 25]
numbers = addToAll(numbers, 5) // [6, 10, 15, 21, 30]
can be turned into a pipeline, like so:
// Same function definitions as above
int[] numbers = [1, 2, 3, 4, 5]
|> square()
|> addToAll(5)
While this may seem pointless for a simple case like this, it becomes incredibly helpful for situations where a piece of data needs to be passed around a lot. For example:
// Some data to be transformed
DataStruct[] newData = data
|> map(someFn)
|> filter(someFilter)
|> sort(someSort)
|> take(10) // Take the first 10 values
Fractions and Decimals
Programming languages have had an issue with how to accurately represent decimal values. Most of them have followed the IEEE 754 standard for floating point values, and that works fine, but it comes with some limitations. I'm sure you've seen 0.1 + 0.2 == 0.3
resolving to false
in most languages, including Python, C++, JavaScript, etc. I won't get into the reason why that is here, but what if we instead looked at math to handle that for us?
In math, any rational decimal value can be represented as a fraction. For example, 0.4
can be written as 4/10
, and 0.333...
can be written as 1/3
. We can use this knowledge to create a Fraction
data structure that looks like this:
struct Fraction {
int numerator
int denominator
}
Given that all numbers have to be rational in traditional programming languages anyway, we can get rid of the issues that irrational numbers like π can cause, simply by rounding them to a sane value. This will also allow values like 1/3
to be represented with full accuracy, and operations like multiplication and division can be massively simplified by multiplying or dividing the numerator or denominator. However, this will slightly complicate the implementations of addition and substraction, as an algorithm for the least common multiple of two denominators will need to be found.
Additionally, a struct will generally be slower to work with than a simple floating point value, but I believe the performance cost may be worth it for the accuracy it will bring. While the float
data type will be consistent with IEEE 754 standards, the decimal
data type (which will be slower but with higher accuracy) will be represented with this Fraction structure. However, whenever the value needs to be displayed, such as when it is printed to the screen, it will be converted to the float
type by default, and can optionally be displayed as a fraction.