Core Functionality
Variables
To create mutable variables, we use the var
keyword. To create immutable constants, we use the const
keyword.
var x = 12
const y = 100
x = 50 // A-Okay
y = 40 // Error
Variables can also be declared without values (they're implicitly set as None
):
var x // Okay
const y // Error - const declarations must have a value
Loops
There are two types of loops, for
and while
.
For loops are constructed like so:
for (1..100) {
// do something
}
for (1..100, index) {
println(index)
}
for (1..100, index, value) {
println(index + value)
}
For loops can also take lists as the first parameter:
const someList = ["a", "b", "c"]
for (someList, index, value) {
if (value == "b") {
println("Found a 'b'")
}
}
While loops are constructed like so:
var i = 0
while (i < 10) {
println(i)
i += 1
}
Both for loops and while loops support the keywords break
and continue
.
break
exits out of the current loop, and continue
skips the current iteration.
Branching
Vortex, like many other languages, supports branching via if/else statements:
var x = 12
var y = 15
if (x > y) {
println("x > y")
} else if (x == y) {
println("x = y")
} else {
println("x < y")
}
Imports
We can perform imports in two separate ways: module imports and variable imports.
Module Imports
Module imports allow you to import an entire file (module) into the current scope. The imported module can be used as an object:
import math : "../modules/math"
const res = math.fib(10)
Variable Imports
Variable imports allow you to pick and choose what you want to import into the local scope:
import [PI, e, fib] : "../modules/math"
const res = PI * e + fib(20)
You can also import all variables from a module into the current scope by simply leaving the import list blank:
import [] : "../modules/math"
Note that both imports have the same path, which is just a path to the module file. Do not include the extension .vtx
in your import paths, the interpreter will do this for you under the hood.
Vortex also has a default module path it looks in if:
No path is specified in the import
@modules
is used inside the import path
On Mac/Linux, the default path is in usr/local/share/vortex/modules/<module_name>
and on Windows it's C:/Program Files/Vortex/modules/<module_name>
.
Built-in modules will always reside in the default modules directory.
import sdl // Vortex looks for "sdl/sdl.vtx" in the default dir
import sdl : "@modules/sdl" // @modules resolves to the default dir path
Variable imports from a built-in module (or from a module the user has placed in the default modules directory) can also target the module's name, without needing a path string:
import [SDLInit] : sdl // Vortex will look for "sdl/sdl.vtx" in the default dir
Data Types
There are multiple data types in Vortex that you can use in your programs:
Number
The Number data type is implemented as a double
, meaning it covers both integers and floating point numbers.
String
Strings can be used to express text and support the majority of escape characters.
Strings also support interpolation:
const name = "John"
const age = 34
const message = f"Hi, my name is ${name} and I am ${age} years old."
println(message)
// Hi, my name is John and I am 34 years old.
Boolean
Booleans are values that can either be true
or false
.
Lists
Lists are vectors that can contain any data type.
You can access list elements with the bracket syntax:
var list = [1, 2, 3]
list[1] = 100
println(list) // [1, 100, 3]
If you attempt to access a list element using a negative index or out of range index, None
is returned.
Setting a value using a negative index will prepend the value, similarly setting a value with an out of range index will append it:
list[-1] = 20
list[100] = 30
println(list) // [20, 1, 100, 3, 30]
However, using the built-in append and insert functions is preferred:
list.append(10)
list.insert(100, 0)
println(lists) // [100, 1, 2, 3, 10]
Lists can also be constructed using destructuring:
const list_a = [1, 2, 3, 4]
const list_b = [...list_a, 5, 6, 7]
println(list_b) // [1, 2, 3, 4, 5, 6, 7]
List destructuring (using the spread operator ...
) can only be used within another list.
Objects
Objects are essentially maps that can hold named properties:
var person = {
firstName: "John",
lastName: "Smith",
age: 34,
"fullName": () => this.firstName + " " + this.lastName,
getAge: () => this.age
}
const name = person.fullName()
const age = person.getAge()
println(name) // John Smith
println(age) // 34
Notice the use of this
in the object. Objects can refer to themselves within their own properties, but only in the context of a function.
Object properties can be constructed using both strings and identifiers.
Properties can be accessed in two ways, through the bracket notation or through the dot notation:
println(person["fullName"]) // "John Smith"
println(person.age) // 34
println(person.height) // None
Notice how attempting to access a property that does not exist simply returns None
.
Object construction can also be done with object destructuring:
const obj_a = {
a: 0,
b: 1,
c: 2
}
const obj_b = {
...obj_a,
c: 2.5,
d: 3,
e: 4
}
println(obj_b) // { a: 0, b: 1, c: 2.5, d: 3, e: 4 }
Object destructuring (using the spread operator ...
) can only be used within another object.
None
None is a special data type that refers to a variable that has no value.
Pointer
Pointer types are used to store raw C pointers. They are predominately used when dealing with external C modules.
Vortex does not support raw memory access, and so these types are reserved mainly for passing around pointers to and from dynamic libraries.
Function
Functions are also core types in Vortex, and can be passed around like other variables.
Functions can act as standard subroutines or as coroutines: Read more about coroutines here.
There is no function keyword in Vortex. All functions are lambdas assigned to variables.
const sayHi = (name) => println(f"Hello, ${name}!")
sayHi("James") // Hello, James!
Function parameters can have defaults, however you cannot declare non-default parameters after the default ones:
const add = (a, b = 10) => a + b
add(1, 2) // 3
add(10) // 20
You can declare a variadic parameter that will capture a variable amount of values. This parameter has to be the last declared parameter:
const doStuff = (...args) => {
println(args)
}
doStuff(1, "a", "b", 3) // [1, "a", "b", 3]
The variables passed in become a list that can then be manipulated.
Another useful thing about variadic parameters is that you can use them to pass an unknown amount of arguments to inner functions:
const outerFunc = (innerFunc, ...args) => {
const start = clock()
const res = innerFunc(...args)
const end = clock() - start
return [res, end * 1000]
}
const inner = (a, b, c) => {
return a + b + c
}
outerFunc(inner, 5, 4, 5).println()
// [14, 0.009]
In the case above, outerFunc
doesn't need to care about how many arguments innerFunc
takes. It simply takes the arguments in args passes them down to the provided function.
If no return value is declared, the function will return None
.
Functions As Constructors
Functions are also used as constructors.
Vortex does not support classes, however class functionality can be achieved by using functions instead:
type Color = (r, g, b, a = 255) => ({
r: r,
g: g,
b: b,
a: a
})
const color = Color(144, 28, 42)
println(info(color).typename) // Color
Constructor functions simply return objects. However, note that the constructor wasn't defined with var
or const
. We used the type
keyword here instead.
There's nothing magical about this keyword. It simply ensures that the object we return from the function receives the same typename as the constructor's name.
Getting the object's typename (via the info() built-in), we can see that it is indeed Color
.
We're not limited to returning an object right away. Remember that Constructors are just functions, so we could have done this instead:
type Color = (r, g, b, a = 255) => {
const cap = (n) => {
if (n < 0) {
return 0
}
if (n > 255) {
return 255
}
return n
}
return {
r: cap(r),
g: cap(g),
b: cap(b)
}
}
const color = Color(144, 28, 42)
Instead of returning an object directly, we declared a cap
function and returned an object with capped values. And since we declared the constructor with the type
keyword, the returned object will have the typename Color
.
Uniform Function Call Syntax
Vortex supports UFCS, allowing you to chain functions together in a seamless way.
Below is a simple example of showing the difference between normal function calls and UFCS:
const join = (a, b) => a + b
// Normal call
join("Hello", " world!") // "Hello world!"
// UFCS
"Hello".join(" world!") // "Hello world!"
This doesn't look too useful yet, however if we wanted to call this function multiple times to construct a longer sentence, UFCS remains easier to read and write:
// Normal call
join(join(join(join("Hello", " world!"), " My name is"), " Jordan"), " and I am 32 years old.") // "Hello world! My name is Jordan and I am 32 years old"
// UFCS
"Hello".join(" world!").join(" My name is").join(" Jordan").join(" and I am 32 years old.") // "Hello world! My name is Jordan and I am 32 years old"
/* Let's make that even clearer */
"Hello"
.join(" world!")
.join(" My name is")
.join(" Jordan")
.join(" and I am 32 years old.")
As you can see, the UFCS version is much more readable. And not only that, but it's much easier to modify. We can swap the calls to join around, add new ones in between, or remove some without dealing with nested calls.
Every function in Vortex can be called in this manner.
Last updated