The Rye programming language is a dynamic scripting language based on REBOL’s ideas, taking inspiration from the Factor language and Unix shell. Rye is written in Go and inherits Go’s concurrency capabilities, including goroutines and channels. Recently, Rye gained support for Go’s select and waitgroups.
Building blocks
Goroutines
Goroutines are lightweight threads of execution that are managed by the Go/Rye runtime. They operate independently, allowing multiple tasks to run concurrently without blocking each other.
Creating a Goroutine in Rye is straightforward. The go keyword is used to launch a new Goroutine, followed by the Rye function to be executed concurrently. For instance, the following code snippet creates and starts a Goroutine that prints a message after a delay:
; # Hello Goroutine
print "Starting Goroutine"
go does { ; does creates a function without arguments
sleep 1000
print "Hello from Goroutine!"
}
print "Sleeping for 2 seconds"
sleep 2000
Prints:
Starting Goroutine
Sleeping for 2 seconds
Hello from Goroutine!
Channels
Channels play a crucial role in coordinating Goroutines and ensuring data exchange between them. They act as a conduit for messages, ensuring that data is transferred between Goroutines in a safe and predictable manner.
To create a Channel in Rye, use the channel constructor function followed by the buffer size. For example, the following code snippet creates a Channel with no buffer:
; # Hello Channel
chan: channel 0
go-with chan fn { c } {
print read c
}
sleep 1000
send chan "Hello over Channel!"
sleep 1000
Prints after 1 seconds and sleeps for another:
Hello over Channel!
To send data through a Channel, use the send method. The receiver side uses the read method to read a Rye value from the Channel.
Select
The select statement is a tool for managing multiple Channels and responding to data availability. It allows Go/Rye to wait for data on multiple Channels and take action accordingly.
The select statement takes a sequence of cases, each containing a Channel expression and an associated block of code. When data becomes available on a Channel, the corresponding case is executed, allowing the Goroutine to handle incoming data from multiple sources.
; # Hello Select
c1: channel 0
c2: channel 0
go fn { } {
sleep 1000
send c1 "Hello"
}
go fn { } {
sleep 2000
send c2 "Bonjour"
}
loop 2 {
select {
c1 { .printv "Channel 1: {}" }
c2 { .printv "Channel 2: {}" }
}
}
Prints first line after a second, then second after another and exits:
Channel 1: Hello
Channel 2: Bonjour
Select also supports a default case, as you will see in the example below.
Waitgroups
Waitgroups are a way for goroutines to synchronize their completion. They allow a goroutine to wait until a set of other goroutines have finished. With waitgroup you can
; # Hello Waitgroup
wg: waitgroup
work: fn { id } {
printv id "Worker {} starting"
sleep 1000
printv id "Worker {} done"
wg .done
}
loop 5 { :i
wg .add 1
go-with i ?work
}
print "Waiting for workers to finish"
wg .wait
print "Finished!"
Prints with second delay betweet worker starting and done:
Worker 1 starting
Worker 2 starting
Worker 3 starting
Waiting for workers to finish
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 3 done
Worker 2 done
Worker 5 done
Worker 4 done
Finished!
Example: Every Second News
In this example, a goroutine is spawned that generates two breaking news messages and sends them to a channel named c.
The main script uses Select to continuously monitor the channel, but if there is no message in the channel, it uses the Select’s default block to print [no news]
and then sleeps for 1 second.
; Every second news
news: channel 0
go fn { } {
sleep 1200
send news "BREAKING NEWS"
sleep 2100
send news "MORE BREAKING NEWS"
}
loop 10 {
select {
news { .print }
_ {
print "[no news]"
sleep 1000
}
}
}
Output, aproximately line per second …
[no news]
[no news]
[no news]
BREAKING NEWS
[no news]
[no news]
[no news]
MORE BREAKING NEWS
[no news]
[no news]
Example: Concurrent Downloader
This example creates a waitgroup wg to track the completion of download tasks.
And the function download that accepts URL as an anonymous injected argument. It prints a message about starting the download, calls generic get method on the URL (generic methods dispatch on the kind of the first argument), retrieves the length of the downloaded text and prints the Loaded message. Then it sends .done to waitgroup.
Script iterates over a list of URLs (sites) and utilizes the go-with function to spawn a goroutine, and add 1 to waitgroup for each URL.
After all download tasks are initiated, the script calls the wait function on the waitgroup, ensuring that the main script doesn’t exit until all download tasks have completed.
; # Downloader with a waitgroup
wg: waitgroup
download: fn1 {
.printv "Started loading {}." ,
.get .length? :len ,
.printv "Loaded {}, Num of characters: " + len
wg .done
}
sites: {
https://www.google.com
https://www.yahoo.com
https://www.duckduckgo.com
}
for sites { .go-with ?download , wg .add 1 }
print "Waiting..."
wait wg
print "Stopped waiting!"
Prints with delays between downloads starting and finishing:
Started loading www.google.com.
Started loading www.yahoo.com.
Started loading www.duckduckgo.com.
Waiting...
Loaded www.google.com, Num of characters: 18509
Loaded www.duckduckgo.com, Num of characters: 6602
Loaded www.yahoo.com, Num of characters: 1582695
Stopped waiting!
Wg .Done
These examples demonstrate Rye’s ability to leverage Go’s native Goroutines, Channels, Select, and Waitgroups to implement concurrent tasks efficiently. I believe a dynamic scripting language with solid Go-like concurrency features has some benefits.
Additional Resources:
For more examples of Goroutines in Rye, visit the Rye examples repository on GitHub: https://github.com/refaktor/rye/tree/main/examples/goroutines
If this got you interested in Rye - the language, visit our website’s main page, or documentation that is sill being written Meet Rye.