Syntax
The overall idea for Risor syntax is that it is like Go, but aims to be more
concise and expressive for scripting. Variables are dynamically typed and all
Risor object types implement an object.Object
interface defined in Go.
Variable Declarations
Variables are declared using the := operator. Unlike in Go, all variables are dynamically typed.
// Declare a variable `x` and assign the integer value 10
x := 10
// Declare a variable `y` and assign the string value "hello"
y := "hello"
// Declare a variable `array` and assign an array of integers
array := [1, 2, 3]
Constant declarations are also supported:
const url = "https://example.com"
Variable Assignment
Existing variables can be set using the = operator:
// Declare `x`
x := 10
// Update `x` to 20
x = 20
Functions
Functions in Risor are declared using the func
keyword. Type declarations are
not supported.
func last(data) {
if len(data) == 0 {
return nil
}
return data[-1]
}
Default parameter values are supported, with allowed types including strings, integers, floats, and booleans:
func greet(name='world') {
return 'Hello, ' + name
}
Unlike in Go, functions can only return a single value. You can, however, return a list and unpack it:
func swap(a, b) {
return [b, a]
}
x, y := swap(10, 20)
print(x, y) // 20 10
Functions may be assigned to a variable and passed as values:
add := func(a, b) {
return a + b
}
result := add(10, 20) // 30
Control Flow
Risor supports most forms of control flow present in Go.
x := 10
threshold := 5
if x > threshold {
print("greater")
} else if x == threshold {
print("equal")
} else {
print("lesser")
}
One difference with Risor is that if-else and switch are expressions in Risor, meaning they evaluate to a value.
x := if 10 > 5 {
"greater"
} else {
"lesser"
}
print(x) # "greater"
name := "Alice"
x := switch name {
case "Joe":
"Hello, Joe"
default:
"Hello, stranger"
}
print(x) # "Hello, Joe"
Ternary expressions are supported:
x := 10 > 5 ? 1 : 0
print(x) // 1
Loops
Risor supports for loops and range loops in the same way as Go.
for i := 0; i < 5; i++ {
print(i)
}
i := 0
for i < 5 {
print(i)
i++
}
array := [1, 2, 3]
for i, value := range array {
print(i, value)
}
for {
print("infinite loop")
time.sleep(1)
}
break
and continue
keywords are also supported.
for i := 0; i < 10; i++ {
if i == 5 {
break
}
print(i)
}
for i := 0; i < 10; i++ {
if i == 5 {
continue
}
print(i)
}
Errors
Risor errors are similar to exceptions in languages like Python and Javascript.
This makes Risor more concise and suitable for scripts, as compared to Go. A
built-in try
function is provided to attempt an operation and fallback to a
default value if it fails. try
accepts any number of functions, and the first
one that does not raise an error has its result returned. If a non-function value
is reached, it is returned immediately.
result := try(func() { error("kaboom") }, "default value")
print(result) # "default value"
Errors can be raised using the error
function:
func divide(a, b) {
if b == 0 {
error("bad idea: division by zero")
}
return a / b
}
Also see the Try-Catch Error Handling section for more information.
Pipe Expressions
Risor supports pipe expressions, which allow you to chain function calls
together. The pipe operator |
takes the result of the expression on its left
and passes it as the first argument to the function on its right.
result := "hello" | strings.to_upper
print(result) # "HELLO"
Importing Modules
Modules are imported using the import
keyword.
import mymod
import mymod as mod
By default the Risor CLI will look for modules as .risor
files in the
current working directory.
You can also import specific attributes from a module using the from
keyword.
from mymod import myfunc
from mymod import myfunc as f
Strings
Simple strings are declared using double quotes, as in Go:
s := "hello"
Multiline, raw strings are also supported using backticks:
s := `
"one"
"two"
`
Templated strings are defined using single quotes, and variables are interpolated between curly braces:
name := "Joe"
s := 'Hello, {name}'
Data Structures
Risor supports lists, maps, and sets.
// Values contained in data structures can hold mixed types
mylist := [1, "two", 3.0]
// Map keys are always strings
mymap := {
"one": 1,
"two": 2,
"three": 3,
}
othermap := {
foo: "bar" // keys can be unquoted if they are valid identifiers
}
myset := {1, 2, 3}
Membership tests are performed using the in
operator:
mylist := [1, 2, 3]
print(1 in mylist) // true
Comments
Single-line comments are defined using //
or #
, and multiline comments are
defined using /*
and */
.
// This is a single-line comment
# This is also a single-line comment
/*
This is a multiline comment
*/
Channels
Risor supports Go-style channels for concurrent communication:
// Create a buffered channel with capacity 1
c := chan(1)
// Send a value to the channel
c <- "hello"
// Receive a value from the channel
x := <-c
// Close a channel
close(c)
Risor channels behave like Go channels in many ways, however they support sending and receiving any Risor object type. Also when ranging over a channel, in Risor you receive both the index and value of the current item.
func work(count) {
mychan := chan(5)
go func() {
for i := 0; i < count; i++ {
mychan <- rand.float()
}
close(mychan)
}()
return mychan
}
// Different from Go: ranging over a channel yields both the index and value
for i, value := range work(5) {
print(i, value)
}
Goroutines
Risor supports launching goroutines using the go
keyword:
// Launch a goroutine
go func() {
// Do some work
}()
// Use a channel for communication
c := chan(1)
go func() {
c <- 42
}()
x := <-c
Spawn
The spawn
function spawns a new goroutine and returns a thread
object. The
thread object can be used to wait for the goroutine to finish and retrieve its
return value.
// Spawn a function and wait for its result
result := spawn(func(x) { return x * 2 }, 21).wait()
print(result) // 42
// Spawn multiple functions
threads := []
for i := 0; i < 5; i++ {
threads.append(spawn(func(x) { return x ** 2 }, i))
}
results := threads.map(func(t) { t.wait() })
print(results) // [0, 1, 4, 9, 16]
Defer
The defer
statement allows you to schedule a function call to be run after the
current function completes:
func example() {
defer print("This will be printed last")
print("This will be printed first")
}
example()
Try-Catch Error Handling
Risor provides a try
function for error handling:
result := try(
func() {
// Code that might raise an error
error("Something went wrong")
},
func(err) {
// Error handler
print("Caught error:", err)
return "fallback value"
}
)
See the try built-in for more information.