Rye

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

About Rye

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

Change Hello World to Hello Mars and print it.
"Hello World" .replace "World" "Mars" |print
; prints "Hello Mars"
Find the Sum of unique numbers in a string.
"12 8 12 16 8 6" .load .unique .sum
; returns 42
Use switch function to get the sound of the animal.
switch 'cow { dog { "woof" } cow { "mooo" } cat { "meow" } }
; returns mooo
Extract combination of one number and one letter from string.
regexp "([0-9][a-z])" |submatch? "-7--x3--0k-r--"
; returns 0k
Print out names starting with A.
{ "Anne" "Joan" "Adam" } |filter { .first = "A" }
|for { .print } 

IO, Spreadsheet, HTTP client

Read a file, print the number of lines and the last 5 lines.
read\lines %data.txt |with
{ .length? .print , .tail 5 |print }
Load a CSV, get average score of all players of level 7.
load\csv %ryebots.csv |where-equal 'level 7
|column? 'score |avg
Load a webpage, save it to a file, print out all the links.
get https://ryelang.org |write* %page.html
|reader |parse-html { <a> [ .attr? 'href |print ] }
Load JSON over HTTP nad extract information, in a Goroutine
go does { get https://httpbin.org/get |parse-json
  -> "headers" -> "User-Agent" |print } print "Loading ..."

HTTP, SMTP server

Serve folder over HTTP
http-server ":8082"
|handle "/" new-static-handler %public_html
|serve
Web server that responds with current time.
http-server ":8081"
|handle "/time" fn { w r } { .write to-string now }
|serve
Start smtp server and print notification on new email.
handler: fn { mail from to origin } {
printv from "new mail from {}" }
smtp-server ":25" |serve ?handler "demo"
      

Dialects

Validation dialect
dict { name: "anakin" } |validate { 
   name: required calc { .capitalize } 
   score: optional 0 integer }
; returns { name: "Anakin" score: 0 }	  
Math dialect.
; 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
SQL dialect.
rye .args .first :id
open sqlite://data.db
|query { select * from operator where id = ?id }

GUI

See the Cookbook for many more GUI Examples.
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
	}
}
See the Cookbook for many more GUI Examples.
 
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
}
 

Delve deeper

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 RyeFocuses on Rye language, it's basics, concepts and components
Rye CookbookFocuses on practical uses and specific technologies
Function referenceBasic documentation and simple examples for many core functions

Get Rye

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 64arm 64wasm
Linuxv0.0.81
Windowsv0.0.81
Mac OSv0.0.81 v0.0.81
/v0.0.81
Homebrewbrew install ryelang
Dockerdocker pull ghcr.io/refaktor/rye:latest

Follow development

Rye lives on GitHub.

Visit, star, report issues, or contribute! Building from source is easy with Go. Dee the README for instructions..

Bateries included

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

Extensions

Rye can be extended internally or externally. External extension are developed in their own repositories:

Fyne GUI frameworkRye-fyne repository Download: Linux MacOS Windows
Gio UI frameworkRye-gio repository
Ebitengine game engineRye-ebitengine repository
RyegenRyegen repository

Related sites

Github Blog Reddit Old blog Asciinema

Rye principles #

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.

Rye is flexible about the language

Everything is an Expression

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]

Code is Data

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

First-Class Everything

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

Functions are all you need

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

Rye is strict about state handling

Constants by default

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!" }
}

Explicit about modifying words

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

Constrained in-place modification

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" }

No direct changes outside of current scope

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"

Rye has no Null, but a very rich Failure value

Null lacks specificity

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

Failure is information

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" } 

Failure enforces resolution

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", } } 

Higher level languages need higher level value types

If a language tries to be more high-level, closer to humans, it need value types closer to humans.

Table value type

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!

Join

If this sparked your interest, join or star us on GitHub, join our Reddit group or just send me an email.