What’s Rye? A flexible, high-level programming language designed for predictible state management
How it’s different? Code-as-data, keyword-less, function centric, 41 datatypes, multiple DSL-s, strict state handling
Rye console demos
Tables •
Exploring JSON •
HN / Somafm in console •
Testing vectors •
TODO in a REPL •
Per projectPer builds •
Eyr dialect
Rye takes ideas from Rebol, and adds few from Factor, Linux shell, and Go. It's in active development, but our goal is to make it practical and powerful ASAP.
Written in Go, Rye doubles as a scripting companion for Go programs. Go's libraries integrate seamlessly, and Rye can be embedded as a scripting or config language for Go programs.
As a high-level language, Rye explores bridging coding and user interfaces. Its interactive console features syntax highlighting, history, tab completion, and tools to explore the language and environment you’re crafting.
Short Rye examples
"Hello World" .replace "World" "Mars" |print
; prints "Hello Mars"
"12 8 12 16 8 6" .load .unique .sum
; returns 42
switch 'cow { dog { "woof" } cow { "mooo" } cat { "meow" } }
; returns mooo
regexp "([0-9][a-z])" |submatch? "-7--x3--0k-r--"
; returns 0k
{ "Anne" "Joan" "Adam" } |filter { .first = "A" }
|for { .print }
IO, Spreadsheet, HTTP client
read\lines %data.txt |with
{ .length? .print , .tail 5 |print }
load\csv %ryebots.csv |where-equal 'level 7
|column? 'score |avg
get https://ryelang.org |write* %page.html
|reader |parse-html { <a> [ .attr? 'href |print ] }
go does { get https://httpbin.org/get |parse-json
-> "headers" -> "User-Agent" |print } print "Loading ..."
HTTP, SMTP server
http-server ":8082"
|handle "/" new-static-handler %public_html
|serve
http-server ":8081"
|handle "/time" fn { w r } { .write to-string now }
|serve
handler: fn { mail from to origin } {
printv from "new mail from {}" }
smtp-server ":25" |serve ?handler "demo"
Dialects
dict { name: "anakin" } |validate {
name: required calc { .capitalize }
score: optional 0 integer }
; returns { name: "Anakin" score: 0 }
; Rye like Rebol or Lisp has no operator precedence.
; But it has Math dialect which has it and more.
math { 2 + 2 * sqrt ( 12 + ( 24 / 3 ) ) }
; returns 42
rye .args .first :id
open sqlite://data.db
|query { select * from operator where id = ?id }
GUI
do\in fyne {
lab: label "I'm Waiting ..."
btn: button "Click here" does { lab .set-text "Finally ..." }
box: v-box [ lab layout-spacer btn ]
with app .window "Button" {
.resize size 200.0 100.0 ,
.set-content box ,
.show-and-run
}
}
do\in fyne {
app .window "Percentage Clock" :win
cont: v-box [
label "This Year [days]" :ly
progress-bar :py
label "This month [days]"
progress-bar :pM
label "Today [hours]"
progress-bar :ph
label "This hour [minutes]"
progress-bar :pm
label "This minute [seconds]"
progress-bar :ps
]
m-of: ?multiple-of
is-leap-year: fn { y } { all { y .m-of 4 not y .m-of 100 not y .m-of 400 } }
days-in: fn { y } { .is-leap-year .either { 366 } { 365 } }
go fn\par { } current {
forever {
with n:: now {
.year? ::y |concat* "Year " |set-text* ly ,
.year-day? / days-in y |set-value* py ,
.day? / days-in-month? n |set-value* pM ,
.hour? / 24 |set-value* ph ,
.minute? / 60 |set-value* pm ,
.second? / 60 |set-value* ps
sleep 500
}
}
}
win |resize size 300.0 200.0 |set-content cont |show-and-run
}
If examples above made you interested, you can delve deeper into the Rye language and Rye runtime. You have few sources for that, both are still work-in-progress, so check back later for more and better content:
Meet Rye | Focuses on Rye language, it's basics, concepts and components |
Rye Cookbook | Focuses on practical uses and specific technologies |
Function reference | Basic documentation and simple examples for many core functions |
Rye is primarily developed on Linux, but can also run on Mac OS and Windows. Rye also runs in web-browsers (Wasm) and Docker. Rye was compiled for mobile (Android and iOS).
x86 64 | arm 64 | wasm | |
---|---|---|---|
Linux | v0.0.81 | ||
Windows | v0.0.81 | ||
Mac OS | v0.0.81 | v0.0.81 | |
/ | v0.0.81 |
Homebrew | brew install ryelang |
Docker | docker pull ghcr.io/refaktor/rye:latest |
Rye lives on GitHub.
Visit, star, report issues, or contribute! Building from source is easy with Go. Dee the README for instructions..
Rye's main binary comes with core language functions and also a lot of bindings already included. Below are some examples of what's already included:
AWS Bcrypt BSON Crypto FT search Goroutines HTML parser HTTP servers JSON MySQL Psql Postmark Regexp SMTP server SXML SQLite
Rye can be extended internally or externally. External extension are developed in their own repositories:
Fyne GUI framework | Rye-fyne repository | Download: Linux MacOS Windows |
Gio UI framework | Rye-gio repository | |
Ebitengine game engine | Rye-ebitengine repository | |
Ryegen | Ryegen repository |
Github Blog Reddit Old blog Asciinema
Rye is designed around a few core principles that should make it expressive, but predictable. While we are still refining details, these ideas shape how Rye behaves and why it feels different from other languages.
These principles are not the features of Rye, look at Meet Rye and the Cookbook for that, but they do dictate the features and design decisions.
All constructs in Rye return a value - including control flow, I/O and assignment - allowing more composable code and less intermediate state.
; you can print (or probe) inline, probe is better for debugging
apples: print 12
; prints 12 and assigns 12 to word apples
; and assign inline
fruits: apples + oranges: 21
; assigns 21 to oranges and 33 to fruits
; either is a conditional function like if/else
name: "Bob"
print either name = "Bob" { "Hello Bob" } { "Locked" }
; prints Hello Bob
; expressions compose nicely and linearly
range 1 10 ; returns numbers from 1 to 10
range 1 10 |filter { .math/is-prime } ; returns prime numbers from 1 to 10
range 1 10 |filter { .math/is-prime } |map fn { n } { 1 / n } ; returns reciprocals of primes
range 1 10 |filter { .math/is-prime } |map fn { n } { 1 / n } |sum |probe
; prints sum of reciprocals of primes:
; [Decimal: 1.176190]
Rye code consists of Rye values (e.g., blocks, words, literal values, ...). There is no difference between Rye code and Rye data. This brings internal consistency and options for code introspection, but we don't want to abuse it.
thats-true: { capitalize "that's true" } ; a word with colon on the right or left is a s set-word
do thats-true
; returns "That's true"
print-thats-true: { print } ++ thats-true
if 1 > 0 print-thats-true
; prints That's true
; a simple rule engine for a water tank
rules: [
context { condition: { temp > 0 |and level < 70 } action: { print "OPEN" } }
context { condition: { temp < 0 |or level > 99 } action: { print "CLOSE" } }
]
temp: 23 , level: 45
for rules { ::rule
if do rule/condition rule/action
}
; prints OPEN
Words, functions, blocks of code, scopes (contexts), and literals are all values that can be created, passed, returned, or assigned. Every value of Rye Runtime is also accessible to the language itself.
some-word: 'this-word
some-func: fn { x } { x + 1 }
inspect-fn: fn { f } {
print2 "Code: " dump ?f
print2 "Result f(100): " f 100
}
inspect-fn ?some-func
; prints:
; Code: fn { x } { x + 1 }
; Result f(100): 101
; we will define tank-1 as a context (scope) to make previous example cleaner
rules: [
context { condition: { temp > 0 |and level < 70 } action: { open } }
context { condition: { temp < 0 |or level > 99 } action: { close } }
]
tank-1: context { temp: 31 level: 100 open: does { print "OPENING" } close: does { print "CLOSING" } }
for rules { ::rule
do\par tank-1 { ; do\par sets context tank-1 as a parent context while evaluating (doing) the code
if do rule/condition rule/action
}
}
; prints CLOSING
Rye has no keywords and no special forms. For example if, loop, fn, try, return are all built-in functions and every active component of the language is just a built-in or an ordinary function call. This makes language very symmetrical, consistent and editable. If special forms are just functions you can always make your own.
probe ?if
; Prints:
; [Pure Builtin(2): Executes a block of code if the condition is true, returning the result of the block or false.]
probe ?fn
; Prints:
; [Pure Builtin(2): Creates a function with named parameters specified in the first block and code in the second block.]
; our custom if
if-joe: fn { n code } { if n = "Joe" { do code } }
if-joe "Joe" { print "Hey" }
; prints Hey
flip-flop-loop: fn { n a b } { .loop { .is-odd .either { a } { b } |do } }
flip-flop-loop 5 { prns "Tik" } { prns "Tok" }
; Prints:
; Tik Tok Tik Tok Tik
Words you define with set-words (single colon) are constants and can't be redefined in given context. This being the default gives you certainty that most things can't change under your feet. This reduces the amount of code you need to look at, to be certain about the effects of the code you are working with or looking at.
name: "Tango"
name: "Cash"
; produces error, you can't use a set-word (one colon) to set an already set word
name:: "Cash" ; word with two colons is a mod-word
; also produces error, use of mod-word is correct, but name is a constant in given context
; you need to use var, or mod-word directly to define a variable word
var 'current-month "January"
current-month:: "February"
current-year:: 2024 ; mod-word also creates a variable word if not yet created
current-year:: 2025
; functions being constants by default is what we usually want
cant-redefine: fn { } { "me" }
; context and function created using set-words are constants
person: context {
say-hi: does { print "Hi!" }
}
As we've demonstrated above, set-words can only set words once. Even if word is a variable, you need a mod-word (double colon), which is visually explicit and visually costly, so you only use it when you have a concrete reason.
{ "jim" "jane" "oto" } .for { :name , print capitalize name }
; produces Error, set-word 'name' can't set an already set word
; sometimes you need a mod-word
{ "jim" "jane" "oto" } .for { ::name , print capitalize name }
; prints:
; Jim
; Jane
; Oto
; but a lot of those cases can be solved more directly and with no assignment
{ "jim" "jane" "oto" } .for { .capitalize .print }
; prints:
; Jim
; Jane
; Oto
Rye encourages immutable operations. Almost all functions return new values rather than modify data. However, for specific cases, in-place modification is possible but again visually explicit. Functions that mutate values must end with ! (e.g., append! change! inc!)
; many cases where you would use append! have much better solutions
names: { "jane" "anne" "john" }
; you don't want to do this
jay-s: { }
for names {
::n
if first n = "j" { append! jay-s n }
}
; if you can do it in more declarative and intent expressive way
filter names { .first = "j" }
Rye enforces strict scope isolation, meaning you can't directly modify variables in outer or nested scopes using assignment words (set or mod-words). Restriction prevents unexpected side effects and makes code easier and again more local to reason about. Instead, Rye encourages explicit and controlled ways to interact with other scopes. In case of modification these means calling functions, again ending with !.
var 'name "EU"
country: context {
var 'name "Slovenia"
change-name!: fn { n } { .change! 'name }
}
print name
; prints EU
print country/name
; prints Slovenia
name:: "BRICS"
country/name:: "Brasil"
; Error - this syntax doesn't even exist
country/change-name! "China"
Null "a billion dollar mistake" has two main problems. It holds no information, so meaning is only assumed. Null can also be passed forward silently, causing unexpected problems later, than it was unknowingly created.
Unlike Python/Java, Rye forces you to handle missing data explicitly via Failures. Rye functions either return a desired value or they return a Failure (also a type of Rye value).
person: dict { name: "Anne" }
print "name" <- person
; prints Anne
; returns None in Python (or Java)
print "surname" <- person
; Produces an Error "missing key", not none
match? regexp "[0-9]*" "abc"
; returns Failure "no result", not none
Contrary to Null, Rye Failure can be rich with information. A failure can have a message (string), a type (word), even Failure code (integer). It can also hold details of exact failure. And it can reference a parent failure, so lower level failures can translate to higher level ones as they move up the stack.
failure 404 ; Constructs a failure with status code 404
fail "user not found" ; Fails with a failure with specific message
fail { 403 wrong-signin-data } ; Fails with failure w/ specific code and type
read %user.txt |check { "Couldn't read user file" }
; If user.txt is missing it returns a nested Failure
; Failure: Couldn't read user file
; └ Failure: user.txt: no such file or directory in builtin file-schema//read
validate dict { score: "10A" } { score: required integer }
; returns a Failure with status 403, type 'validation-failure
; and details: { score: "not integer" }
Unlike Null, which can be silently passed around, Rye forces you to address Failures immediately.
You can use functions like fix (to provide default values), check (to check for failure and add context to it if it happens), or disarm (to deactivate the Failure and access it's data) to handle Failures gracefully.
If failure is not handeled it elevates into an Error, our program enters an unpredicted state and generally needs to be stopped and Error fixed in code.
person -> "surname" |print
; produces a Failure and because it's not handeled and Error happens
; Fix returns an original value, or result of a block in case of a failure
person -> "name" |fix { "John" } |print
; prints Anne
person -> "surname" |fix { "Doe" } |print
; print Doe
; We've already seen check in the previous section
; check returns an original value or wraps failure in a higher level failure
; ^check is a returning variant of check, look at Meet Rye for specifics
get-user-data: fn { uid } {
read to-file uid ++ ".json"
|^check "Couldn't read user data."
|parse-json
|^check "Couldn't parse user data."
}
get-user-data "u101" ; in case file is there but it's not proper JSON
; We get a higher (current function) level failure, an in it a lower level failure
; Failure: Couldn't parse user data
; └ Failure: Failed to Unmarshal. in builtin parse-json.
validate dict { score: "10A" } { score: required integer } |disarm |to-json
; returns a JSON:
; { "status": 403, "type": "validation-failure", "data": { "score": "not integer", } }
If a language tries to be more high-level, closer to humans, it need value types closer to humans.
People use tabular format to work with a lot of data, so one of Rye value types is a Table. Visit The Cookbook to learn much more about Tables. This is just a quick example.
spr: load\xlsx %adversaries.xlsx
spr .where-contains 'Species "AI"
|order-by 'Year 'asc
|head 3
|columns? { 'Name 'Keywords }
|order-by 'Name 'asc
|display
;
; | Name | Keywords |
; +------------------------------------------------+
; | HAL 9000 | Calculating, Logical, Fatal |
; | Ultron | Ambitious, Intelligent, Destructive |
; | VIKI | Logical, Overbearing, Ruthless |
If this got you interested, Meet Rye is a good next step!
If this sparked your interest, join or star us on GitHub, join our Reddit group or just send me an email.