Below are 3 examples, that show some of the principles of working with Rye language. But first, some terminology.

Code is data / data is code

Definition: A language is homoiconic if a program written in it can be manipulated as data using the language.

Lisps, schemes, REBOL and Rye are homoiconic, but what does this actually benefit us?

After two decades of programming in such languages, it’s hard to answer this quickly. After a while, you don’t think about or even notice the homoiconicity, it just seems the way things should be anyway. Try asking a fish to sell you on the idea of living in water.

Why would I express my thoughts about the world with a substrate, that can’t also expressing thoughts about the substrate itself?

pencil drawn by a pencil

While Lisps emphasize the ‘code is data / data is code’ paradigm at compile time through macros, REBOL and Rye usually leverage this concept dynamically at runtime. And it’s not just about code. It’s about all live constructs of the language being fully accessible to the language itself.

I’m not sure it’s correct, but when explaining the difference I often used the phrase runtime homoiconic.

Common practices still apply

Homoiconicity affects the way you think about your program, but it affects the macro view, the approach, the way you declare the problem. On the level of code, good practices, like those from functional programming, still fully apply.

For example, you really don’t want to manipulate a block of data by injecting a bunch of words between values and evalating that as code. But you could, and you probably did at some point early on.

; don't do this!
data: { "don't" "do" "this" }

; in Rye you can't mutate blocks that easily anyway
; so we basically have to generate a new block
data .map { .concat* { print } } |unpack :code |dump |print
; prints:
;  { print "don't" print "do" print "this" }
do code
; prints:
;  don't
;  do
;  this

Later you see any creative manipulation of code as a strong code smell. Except for specific cases, where you explicitly do want to generate code. Normal approach, application of functions, solves things in cleaner and more robust way.

; still, just do this
data: { "still" "just do" "this" }

; a simple for loop should do
data .for { .print }
; prints:
;  still
;  just do
;  this

But there are still examples where this runtime homoiconicity shows some (positive) teeth. I will try to show a few of them.

Detour: REBOL’s parse DSL

We don’t have REBOL’s parse function/DSL in Rye for now. It’s a well known awesome thing, so we test nothing by adding it at this phase, and the lack of it forces us to think creatively around it. One danger of having parse is, that you then try use it for everything.

And parse is a language on it’s own, in REBOL we had parse gurus, that could do magic with it. Red, the successor of Rebol, took parse and still made it even more powerful. Check out these, if you are interested:

REBOL docs, Red blogpost, Red by example, REBOL docs 2

; A REBOL and Red example that validates math expressions

expr:    [term ["+" | "-"] expr | term]
term:    [factor ["*" | "/"] term | factor]
factor:  [primary "**" factor | primary]
primary: [some digit | "(" expr ")"]
digit:   charset "0123456789"

parse "1+2*(3-2)/4" expr        ; will return true
parse "1-(3/)+2" expr           ; will return false

Parse is used for parsing strings, binary input and most importantly also blocks of REBOL values (the REBOL data/code). It’s also the main tool for creating DSLs.

rule: [
    set action ['buy | 'sell]
    set number integer!
    'shares 'at
    set price money!
    (either action = 'sell [
            print ["income" price * number]
            total: total + (price * number)
        ][
            print ["cost" price * number]
            total: total - (price * number)
        ]
    )
]

total: 0
parse [sell 100 shares at $123.45] rule
print ["total:" total]

And parse itself is a DLS (dialect) … cool right :)

Example 1: xmlprint DLS

At my job, I needed to create SEPA XML documents. Especially the various design-by-committe XML standards can be quite verbose, and these managed also to be cryptic with it’s naming scheme. One of schemas I was working on is even called pain.008.001.02 🤷

Previous solution

In the past I created (REBOL) DSLs to generate XML using words and blocks with a more conventional structure:

  subscribers {
	person { 'id 10 
	  name "Elanor"
	  surname "Brandyfoot"
	}
	person { 'id 11
	  name "Poppy"
	  surname "Proudfellow"
	}
  }

that would generate

  <subscribers>
	<person id="10">
	  <name>Elanor</name>
	  <surname>Brandyfoot</surname>
	</person>
	<person id="11">
	  <name>Poppy</name>
	  <surname>Proudfellow</name>
	</person>
  </subscribers>

But with current XML schema, I thought the least error prone way to turn it into code was to just copy whole example and work on that, not really touching or writing the cryptic tagnames by myself (see for yourself xml schema).

The DLS

Rye already had two word types that looked like XML starting and ending tag (Xword and EXword ) and which I could use for this.

So I came up with this simple DSL. Note that template is not a string, but a regular block of Rye values / code. In this case it consists of xwords, exwords, blocks and strings.

template: {
   <data>	
	<header>
	  <event> ( header/name ) </event>
	  "Date of the event"
	  <date> ( header/date ) </date>
	  <type>
 	    <subtype> ( header/type ) </subtype>
	  </type>
	</header>
	<person>
  	  <fullname> ( .fullname ) </fullname>
	  <name type="first"> ( -> "name" ) </name>
	</person>
   </data>
}

fullname: fn { d } { join\with [ d -> "name"  d -> "surname" ] " " }

header: context { name: "Crowning"  date: now  type: "X" }

xmlprint\full dict { name: "Durin" surname: "Longbeard" } template

xwords and exwords define the structure of the document, blocks get evaluated (with data argument injected into a block) and strings get turned into comments.

I started with creating this DSL first, and only then thinking on how we could evaluate it.

The evaluator

I could create evaluator in Go like all Rye’s DSLs are implemented now, but in REBOL or Red we would use the aswesome parse function I demoed above, and which we don’t have.

So I added a rather simple walk built-in function to Rye.

; private function creates a context evaluates the code within context
; but just returns the result of the code. It can be used to create pockets
; of specialised code that don't spill out
private {
	
	is-not: fn { a b } { .to-word = to-word b |not }
	comment: fn1 { .concat* "<!--" |concat "-->" }
	process: fn\par { block values current } current {
		|pass { blk: false token: false }
		|walk { ::blk
			|peek ::token .type? .switch {
				xword {
					prn token
					process next blk values token
				}
				exword {
					prn token
					if token .is-not current { fail "tag mismatch" }
					next blk
				}
				block {
					with values token |prn
					next blk
				}
				string { 
					prn comment token
					next blk
				}
			}
		}
		newline true
	}
	
	; Only this function will get returned
	fn\par { values block "Accepts dict of values and a block of dialect" } 
	       current { process block values 0 'no }
	
} :xmlprint

xmlprint\full: fn { data tpl } { 
	print `<?xml version="1.0" encoding="UTF-8" ?>`
	xmlprint data tpl 
}

And we can call the script:

-mycomp-$ rye example1-micro.rye
<?xml version="1.0" encoding="UTF-8" ?>
<data><header><event>Crowning</event><!--Date of the event--><date>2025-03-02 17:02:
34</date><type><subtype>X</subtype></type></header><person><fullname>Durin Longbeard
</fullname><name type="first">Durin</name></person></data>

Created XML is without indentation, which has it’s benefits. You can find a few lines longer example that properly indents XML among examples: examples/xmlprint.

DSLs vs. contexts

In REBOL we would use parse DSL to create a DSL above, but as you can see, using just regular Rye, with a walk function worked too.

Parse might have been shorter and it would be able to express more complex patterns without us needing to create a state-machine. But using regular Rye also has benefits. For one, we don’t have to learn another set of rules of evaluation, and we can re-use whole Rye, or just parts of it.

Rye tries to rely much more on smart use of contexts. Contexts (scopes) are first class values in Rye and we can do a lot with them, but we don’t change the rules of evaluation, which are quite rich with just Rye already.

Example 2: testing DSL

I needed a testing tool for Rye. Again I first started thinking about “what is the best way to declare the tests” and only then, how I would evaluate that structure.

The tests

I would like to group tests by name, which will most often be the name of the function I’m testing. I don’t want to name individual tests, because I will just write nonsense there after a while.

I mostly want to test if an expression returns a specific value (equal). I also want to compare stdout (what code test prints out) to a specific value and I want to be able to define that some expressions should return an error.

This is the most minimal way of declaring that I could concoct:


group "math" {
	equal { 10 + 101 } 111
	equal { 123 / 20 } 6.125
	equal { 123 % 20 } 3
	error { "11" + 22 } 
}

group "concat" {
	equal { "Cele" .concat "brimbor" } "Celebrimbor"
	equal { concat 3 + 7 + 9 " rings" } "19 rings"
	equal { { 3 7 } .concat 9 } { 3 7 9 }
	equal { "RON" .concat* "SAU" } "SAURON"
}

group "looping" {
	stdout { loop 3 { prn "Xo" } } "XoXoXo" 
	stdout { { 1 2 3 } .for { .prns } } "1 2 3 " 
	error { 33 .for { .prns } }
}

The Rye code above could be seen as Rye data. Some words, blocks of code and some literal values, and we could create a special evaluator for it, either in Go or Rye.

But group could also just be a function taking two arguments, a name of the group and a block of code.

equal and stdout could be two functions accepting a block of code and a value to compare the result or standard output to.

And error takes just one argument, a block of code that should fail.

The context

Our “DSL” (which it is not) is evaluated as a regular Rye in a specific context. This context has the four functions we mentioned above. Most of the code is basically there to print the expected and received values in case of a failure.

test-framework: context {
	
	group: fn { name code } { prns name , do  code , print "" }
	
	equal: fn { test res } {
		do test :got = res |either
		{ prns "✓" }
		{ prns " ✗ Failed:" 
		  prns join { "expected " inspect res ", got " inspect got } }
	}

	stdout: fn { test res } {
		capture-stdout { do test } :got = res |either
		{ prns "✓" }
		{ prns " ✗ Failed:"
		  prns join { "expected " inspect res ", got " inspect got } }
	}
	
	error: fn { test } {
		try test :got .type? = 'error |either
		{ prns "✓" }
		{ prns " ✗ Failed:"
	      prns join { newline "expected error but got: " inspect got } }
	}
}

%demo.micro.rye .load .do\in* test-framework

To look closer, function group takes two arguments a name of the group and a block of code it prints the name and does (evaluates) the code.

	group: fn { name code } { prns name , do  code , print "" }

Function equal also takes two arguments a block of code (a test expression) and the expected result.

	equal: fn { test res } {
	    ; does the block of code (test), stores result to word got and compares
		; it to expected result
		do test :got = res |either
		; if they match it displays a checkmark
		{ prns "✓" }
		; if not it displays and cross and expected and recieved values
		{ prns " ✗ Failed:" 
		  prns join { "expected " inspect res ", got " inspect got } }
	}

stdout and error functions are not that different. They mostly rely on existing functions capture-stdout and try.

Results

When all test pass we get something like this:

math ✓ ✓ ✓ ✓ 
concat ✓ ✓ ✓ 
looping ✓ ✓ ✓ 

And if we change some tests so they will fail:

    ; just the changed lines
	error { 11 + 22 } 
	equal { "RON" .concat "SAU" } "SAURON"
	stdout { loop 2 { prn "Xo" } } "XoXoXo" 

we get:

math ✓ ✓ ✓  ✗ Failed: expected error but got: [Integer: 33] 
concat ✓ ✓ ✓  ✗ Failed: expected [String: SAURON], got [String: RONSAU] 
looping  ✗ Failed: expected [String: XoXoXo], got [String: XoXo] ✓ ✓ 

We use the same code, but with some improvements and colors to do the normal tests and also generate reference documentation for the website. You can see that code in: info/main.rye

Our tool in action:

rye tests demo

The more cautious will probably ask themselves “what if such DSLs cause naming conflicts?”. We won’t go there in this blogpost, but functions like do\par, fn\par, extend and bind come into play there. In short, each code or function in Rye can be evaluated in a custom chain of contexts.

Example 3: Buttons GUI app

This post is getting extra long, so in this example I will let the code and a screenshot do most of the talking.

Since Rye can navigate and use live contexts (this is also used well in Rye console) I wanted to combine this feature with GUI-s somehow.

The first idea was to make an app that takes a context and displays a button for each function. If you press the button, the function should be called and the output of the function displayed in the window. I’m using Rye-fyne for this. Fyne is a GUI library for Go. The bindings for it were autogenerated using ryegen. So many links … :P

The code below takes a rye file with functions as CLI argument and creates the GUI.

rye .needs { fyne }

myctx: context { do load to-file first rye .args }

do\par fyne { 

	win: app .window "Buttons app"
	gw: grid-wrap size 120.0 36.0 { }	
	spl: h-split gw lbl: label "" ,
	spl .offset! 0.6
	
	for lc\data\ myctx { ::w
		extends myctx {                   ; we evaluate this in local contect
			fnc: ?w                       ; to prevent captured variables problem
			gw .add button to-string w    ; and make it's parent myctx
			  closure { }
			  { set-text lbl capture-stdout [ fnc ] }
		}
	}
	
	with win {
		.resize size 450.0 350.0 ,
		.set-content spl , 
		.show-and-run
	}
}

This is our sample file with Rye functions.

hello: does { print "Hello world!" }
time: does { now .print }
load-avg: does { os/load-avg? .print }

my-ip: fn { } {                     
	get https://api.ipify.org?format=json
	|parse-json -> "ip" |print
}

joke: does {
	get https://official-joke-api.appspot.com/random_joke
	|parse-json
	|with { -> "setup" |print , -> "punchline" |print }
}

And this is the result.

buttons GUI app

I have ideas to extend this and maybe make it useful, or at least interesting. :)

Bravo!

Hats off, if you read all the way to the end.

If this got you interested in Rye, visit our website’s main page, or documentation that is still being written, like Meet Rye or the function reference that is generated from the tests similar to our Example 2.

If you want more long form content about Rye check out these two Cookbook pages:

Thanks!

To @friedcell for finding multiple typos.

And:

“A star on Github never hurt nobody”