Introduction

What is book about?

This book is about getting you started with Ryan, from zero to hero! First of all, before we even start with the language itself, we will show how you can get Ryan to work on your machine. There is a growing list of options available. Then, we will introduce the language syntax, with its most common features. Ryan is a simple and straightforward programming language; you can master it in no time! Lastly, we will show how you can integrate Ryan with your current applications. Probably, you just need to read one or two chapters of this final section, unless you regularly program for a plethora of different languages.

This book is not about implementation details or how to tweak the import system. These more advanced topics deserve a different approach, which is not suitable for people just starting out. If you curious about that, the API documentation is a great place to start, even if Rust is not quite your thing.

Who the hell is Ryan?

Ryan is a minimal programming language that produces JSON (and therefore YAML) as output. It has builtin support for variables, imports and function calls while keeping things simple. The focus of these added features is to reduce code reuse when maintaining a sizable codebase of configuration files. It can also be used as an alternative to creating an overly complex CLI interfaces. Unsure on whether a value should be stored in a file or in an environment variable? Why not declare a huge configuration file with everything in it? You leave the users to decide where the values are coming from, giving them a versatile interface while keeping things simple on your side. Ryan makes that bridge while keeping the user's code short and maintainable.

So, without further ado, shall we begin?

Getting Ryan

There are plenty of different ways to get Ryan. It really depends on what your setup is, since Ryan is designed to be embedded in larger applications, although it also works smoothly as a standalone application. There are therefore, two things that we refer to as Ryan:

  1. The Ryan CLI: a program that you install in your computer. You pass a .ryan in and get .json out.
  2. The Ryan library: a dependency you add to you project that takes a .ryan from somewhere and spits out data that your programming environment can understand.

Depending on your use-case, you might need to install one of the above. Probably most people will want to install both.

Getting the Ryan CLI

One-liner (Linux, MacOS)

Copy and paste the following command in your favorite console:

curl -L -Ssf "https://raw.githubusercontent.com/tokahuke/ryan/main/install/$(uname).sh" \
    | sudo sh

You will need sudo rights to run the script (this installation is system-wide).

Download the binary from GitHub (Linux, MacOS, Windows)

Go to the Ryan repository and download the zipped file corresponding to your platform. Unzip it and move it to somewhere nice!

Using cargo

If you have Cargo, just run:

cargo install ryan-cli

And you are done. You will not even need sudo!

Getting the Ryan library

Depending on your language, you can install a binding to Ryan from your standard package manager:

cargo install ryan          # Rust
pip install ryan-lang       # Python
npm install ryan-lang-web   # JavaScript (web)
npm install ryan-lang-node  # JavaScript (NodeJS)

If a binding is not available to your language, you can always use the Ryan CLI + you favorite JSON parser as a fallback. The Ryan CLI is already thought-out for this kind of programmatic interaction.

Hello... JSON!?

Note: if you are unfamiliar with JSON, you may skip this chapter and go right ahead to the next one. You will still be able to understand what Ryan is all about.

This tutorial is focused on getting you up-to-speed with Ryan as painlessly as possible. Where else then better than a good Hello world, eh?

{
    "words": ["Hello", "World!"]
}

That's right! That is valid Ryan right there. The first thing you should know is that Ryan is a superset of JSON, that same format that grows like weed all over the internet. That is on purpose: Ryan aims to be uncomplicated and familiar.

On the other hand, we all know that JSON can sometimes be a pain to edit. You can't even write a comment on that damn thing! And don't even get me started on multiline strings... Sometimes, JSON is a victim of its most important virtue: being dead simple.

Simple amenities

Ryan, however, can be a bit more complex, trading performance for convenience. In fact, Ryan was made more for humans than for machines. Therefore, in Ryan, you can have stuff like this:

// A comment!

Shocking, I know. Ryan only supports line comments, which start on a // and ends on a line break. There are no block comments, but why would you want one of those anyway?

Oh!, by the way... that last line of Ryan was totally valid. I know it's a bit of a side note, but


is a full ryan program in its own right and evaluates to null.

On the same note, much stuff that you are used to have from JavaScript you can also have here:

[
    "forgiving",
    "commas", 
    1_000.0,    // Forgiving numbers!
]

and

{
    naked: 1,
    dictionary: 2,
    keys: 3,
}

And finally, for everyone's delight,

"multi
line
strings
are allowed!"

... given that they are valid UTF8. However, only double-quoted strings are possible. How could you possibly like those single-quotes aberrations?

Finally, template strings à la JavaScript are also supported. These will make your code way simpler to read:

`template ${"string"}`

The above code evaluates to the string "template string".

Conclusion to the introduction

You might be asking yourself about all the cosmetics and nice gadgets from the last section: "so what? JSON5 offers basically the same thing and YAML does that and tons more. What is the upside here?" Well, Ryan is capable of much more than these simple tricks and that is what we are going to explore in the following chapters. However, if all you got from this book is that Ryan is a nicer JSON, well... that's a start (and definitely a legitimate use for Ryan).

Simple types and simple operations

Numbers

The simplest type you can think about in Ryan is a number. Numbers come in two flavors: integers and floats. Both types are treated differently, although interchangeably via type coercion (more on that later). Here are some examples of numbers:

123     // an integer
1.23    // a float
123.0   // another float, not an integer
-12     // integers can be negative too
1e23    // and floats can be written in scientific notation if they are veery big
1e-42   // ... and also if they are veeery tiny
1_000.0 // if a number is very big, you can sprinkle `_` around for readability.

If you know other programming languages, this is most probably identical to what you have encountered before.

In Ryan, you can write arbitrary numerical expressions mixing and matching floats and integers. When necessary, Ryan will implicitly convert the integer to the corresponding float. This is called type coercion. Here are some example of what you can do:

2 + 3           // add integers
2.0 + 3         // mix and match integers with floats freely
5 - 4 * 3 / 2   // arbitrary operations with the standard precedence
(2 + 3) * 4     // use parentheses
23.0 % 7.0      // modulo operation is supported, even for floats 

Booleans

Booleans indicate a binary choice and come only on two values true or false. They can be operated upon using the three canonical operations and, or and not:

true and false      // -> false
false or false      // -> false
not false           // -> true

Things get even more interesting when you combine test operations on integers and float to produce booleans:

1 == 2              // -> false (tests for equality)
1 != 2              // -> true  (tests for inequality)
1 > 2               // -> false (tests if left is greater than right)
1 >= 2              // -> false (tests if left is greater or equal to right)
1 < 2               // -> true  (tests if left is less than right)
1 <= 2              // -> true  (tests if left is less or equal to right)

And, of course, you can match everything together to create complex boolean expressions:

1 > 2 or 3 > 4          // -> false
not 1 == 2              // -> true
2 % 3 == 2 and 1 < 0    // -> false

if ... then ... else ...

This construction can be used to control the value of an expression based on a condition. The if clause accepts an expression that returns a boolean and then the final result of the expression is the expression after then if true or else if false. Some examples are shown below:

if 1 == 1 then 123 else 456     // -> 123
if 1 != 1 then 123 else 456     // -> 456
if 2 >= 3 then 123              // -> error! There always needs to be an `else`
if 0 then 123 else 456          // -> error! The `if` expression has to be a boolean 

Strings

Strings are pieces of escaped text that can represent any UTF-8 encodable data. They come between " (double quotes) and boast a good deal of escape characters, like \n (new line). If you come from other programming languages, the convention here is probably the same you are used to. Some examples of strings are shown below:

"abc"       // -> abc
"ab\nc"     // -> ab<enter>c
"ab\"c"     // -> ab"c (`\"` is how you write a double quote without being ambiguous)
"multi
line
strings
are
welcome too"
'but single-quotes are not'     // -> error! only double quotes allowed

Strings can be added together for concatenation:

"abc" + "def"       // -> abcdef

But you cannot add numbers and strings together to get the "intended" result:

"there are " + 4 + " lights"    // -> error! Cannot add text and integer

However, you can use the "fmt trick" to achieve the desired result:

"there are " + fmt 4 + " lights"    // there are 4 lights

Or, better still, you can use template strings, where you can interpolate full expressions in the middle of your string, like so:

`there are ${5 - 1} lights`     // there are 4 lights

Note that template strings, unlike normal strings, are escaped using ` and not ".

null

Lastly, but not least, there is the simplest type of all: null. Null has only one value: null and represents the absence of something. Null is not a boolean or an integer, so it will not behave like, say false or 0. Therefore, all these won't work:

1 + null                                // error!
if null then "wrong" else "wronger"     // error!
not null                                // error!
null > 0                                // error!
"The answer is" + null                  // error!

Null is in fact its own unique thing. However you can do much with null via the ? operator. This operator allows you to provide a default value in case some expression of yours, for some reason evaluated to null:

null ? 1    // -> 1
2 ? null    // -> 2
3 ? 2       // -> 3

Variables

If you have ever been to a maths class, you know what a variable is. It's an identifier that refers to a value. For example,

x = 1

In this case, x is one. However, it can get more complicated than that

x = 2 + 3 * 4

The power of variables really shine when you have to use the result of a computation in more than one place. In this case, variables are great at saving you a great deal of trouble:

let x be 2 + 3 * 4 ...

... then this:
x * x + 2 * x + 3
... is much better than this:
(2 + 3 * 4) * (2 + 3 * 4) + 2 * (2 + 3 * 4) + 3

Like most programming languages, Ryan leverages variables to make referring to results of expressions (the same ones we have encountered last chapter) a much more delightful thing than hitting Copy+Paste all the time.

Simple bindings

This is how you declare a variable in Ryan:

let a_guy = "Ryan";

What this line says is that a_guy refers to the value "Ryan" from now on. Now, you can start using it to build more complex stuff on top of it:

let a_guy = "Ryan";
`Hello, ${a_guy}!`

This will evaluate to the text "Hello, Ryan!". The construction let ... = ...; is called a variable binding, becaus it binds the value ("Ryan") to an identifier (a_guy).

Variable names

Variable names in Ryan are represented by text, but not all names are valid. Here are the rules you must follow to create a valid variable name:

  • Variable names can only contain the characters:
    • a-z (lowercase letters)
    • A-Z (uppercase letters),
    • 0-9 (any digit) and...
    • _ (the underscore).
  • Variable names cannot be in a list of reserved keywords. These are names that are already used for other things in Ryan. Some examples of this you have already found:
    • Values like true, false and null.
    • Control words like if, then, else and (out newest acquaintance) let. The list of words is not big and does not contain many reasonable variable names. Ryan will warn you if have chosen an invalid name. Finding a different one should be an easy task (e.g., append a _ to the end of your name).

Of course, even if a variable name is valid, it does not mean that it is a good name. Here are some useful tips when naming your variables:

  • Avoid one-letter names, like x, y and i.
  • You may use more than one word for variable name. When doing so use snake_case or camelCase (either one is fine). dontjustglueverythingtogetherbecauseitsdifficulttoread.

The key is to be expressive but to keep it short.

Variables are immutable, but can be shadowed

In Ryan, all values are immutable. That means that there is no way of changing the value after it was assigned to a variable. This is not true in many programming languages. For example, in Python:

x = "abc"
x = x + "def"
# x is the _same_ variable, but now is "abcdef"

In Ryan, there is something called shadowing, where you can do something like this:

let x = "abc";
let x = x + "def";
// the second x is a different variable than the first x.
// you haven't changed `x`; you just recreated it.

In other words:

  • In many programming languages, one can assign a value to a variable and then mess around with the value or even change it completely.
  • In Ryan, there is no such a thing. When you redeclare a variable, you effectively destroy the old one and create the new one from scratch. The difference is subtle, but (sometimes) it matters. If you are new to the mutability-immutability, this might be too abstract to grasp at first, especially if you are relatively new to the programming business. If you don't get it, don't worry: it's not a big deal. There are few points where it really matters and it will be pointed out explicitly.

Not-so-simple types

Collections are types which are used to aggregate and organize a set of data. Ryan supports two collection types: lists and dictionaries (or maps).

Lists

Lists are a collection of values, sequentially ordered. Here are some lists for you:

[1, 2, 3]       // Lists are a comma-separated sequence of values between brackets.
[1, "a", null]  // You can mix and match the types however you want.
[]              // An empty list is also a list
[
    1,
    2,
    3,  // Use forgiving commas for long lists
]

The two main operations on lists are concatenation (just like with strings) and index accessing. Concatenation is pretty straightforward:

[1, 2, 3] + ["a", "b", "c"]     // -> [1, 2, 3, "a", "b", "c"]

Alternatively, like in some programming languages, Ryan allows you to easily compose lists using flatten expressions (the ... syntax below):

let x = [4, 5, 6];
[1, 2, 3, ...x]     // -> [1, 2, 3, 4, 5, 6]

This yields a similar effect to adding lists.

Index accessing is also easy: get the n-th element in the list. However, Ryan shares a pet-peeve with many other programming languages: the first position is indexed by the number zero.

[1, 2, 3][0]        // -> 1
[1, 2, 3][1]        // -> 2, not 1!
[1, 2, 3][3]        // error! Tried to access index 3 of list of length 3 
                    // (3 is the 4th element!!)

Dictionaries (or maps)

dictionaries are a collection of values indexed by strings. This name, dictionary, is quite apt in describing what it does. Just like the regular old book, it uniquely associates a word to a value. Here is an example of a Ryan dictionary:

{
    "name": "Sir Lancelot of Camelot",
    "quest": "to seek the Holly Grail",
    "favorite_color": "blue"
}

However, the same dictionary is much nicer written this alternative way:

{
    name: "Sir Lancelot of Camelot",
    quest: "to seek the Holly Grail",
    favorite_color: "blue",     // use forgiving commas for extra niceness
}

Whenever the key of a dictionary could be a valid variable name, you can omit the double quotes of the string. This doesn't change the value of dictionary (both examples correspond to the same thing); this is just syntax sugar: an nicer way of expressing the same thing.

Dictionaries have other few different tricks on their sleeves. For example, it is syntactically valid to repeat a key in the dictionary:

{
    a: 1,
    a: 2,
}

However, only the last occurrence of the same key will count to the final result. The above dictionary evaluates to { a: 2 }. You can also use the name of a variable directly, without the value, if the key coincides with the variable name:

let a = 2;

{
    a,      // ... as opposed to `a: a`
}

The above will also evaluate to { a: 2 }.

You can also specify an if guard at the end of each key, in order to make its insertion in the dictionary optional, like so:

{
    a: 1,
    b: 2 if "abc" == "def",     // wrong!
    c: 3 if 1 == 1,             // quite true...
}

This will evaluate to { "a": 1, "c": 3 }.

Lastly, just like with lists, you can concatenate dictionaries and index them in the very same fashion as you would do a list:

let x = { a: 1, b: 2, c: 3 };
let y = { d: 4, e: 5, f: 6 };
x + y       // -> { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }
x["a"]      // -> 1
x["d"]      // error! Key "d" missing in map

And you can also use flatten expressions with dictionaries, just as if you would do with a list:

let x = { a: 1, b: 2, c: 3 };
{
    d: 4,
    e: 5,
    f: 6,
    ...x
}   // -> { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }

This can be useful when creating record inheritance structures in Ryan.

Lastly, if a certain key could be a valid variable name, one can also use the shorter . operator to index dictionaries:

let x = { a: 1, b: 2, c: 3 };
x.a     // -> 1
a.d     // error! Key "d" missing in map

Importing things

Keeping with the theme of reusing code that we introduced when talking about variables, Ryan also you to use a whole .ryan file as a kind of variable in other .ryan files. Imagine that you have a big projects in which you need to have many .ryan files for many different purposes. Normally, it's the case that you need to share a couple of variables between all these files, for example a username or a link to a shared resource. It would be a pain to have to redeclare it in every file, not to say dangerous if you ever need to change its value.

To cope with this scenario, you could create a common.ryan with all the repeated values and then, in each file, just do

let common = import "common.ryan";

This will evaluate the contents of common.ryan and put it in the variable common. The file common.ryan must be in the same directory as the file containing the import statement, otherwise Ryan won't be able to find it. If your file is in another path, you can use your operational system's path system to locate it:

let common = import "../other-stuff/common.ryan";       // e.g, in Linux and MacOS
let common = import "C:\Users\ryan\stuff\common.ryan";  // e.g, in Windows

Customizing your files

Another very common use-case for imports is to customize the JSON generated by your .ryan depending on the environment it is executed in. For example, it's very common for programs to be configured differently when testing than when put to run "for real" (also called the production environment). Usernames, passwords and resource names will be completely different to avoid a rogue test to ruin the operation of your system.

Enter environment variables: these are a set of variables that your operational system passes to every program in order to customize its behavior. Of course, you can set these variable directly through the command line:

MY_VAR=1 ryan < my_file.ryan

This will invoke ryan on my_file.ryan with MY_VAR set to 1. You can then access this variable from your .ryan file as so:

let my_var = import "env:MY_VAR";   // my_var will be set to `1`.

You can pass whole Ryan programs in environment variables, if you wish, although it may not be the most comfortable thing to do. The only restriction is that such programs can only access other environment variables; it cannot touch your files anymore. Therefore, this will not work:

MY_PROG='import "common.ryan"' ryan < my_file.ryan

If my_file.ryan tries to import "env:MY_PROG", an error will be raised.

Importing chunks of text

Up to now, we have only talked about importing Ryans from Ryans. However, in many cases, it is very quite to import text directly, verbatim. Ryan saves you the trouble of writing quotations and escape sequences by allowing you to import things as text:

let gilgamesh = import "tale-of-gilgamesh.txt" as text;

You can also use this with environment imports:

let username = import "env:USERNAME" as text;       // username is `"Ryan"`.

If USERNAME is set to Ryan, without the as text, you would get an error: after all the variable Ryan has not been set in your USERNAME program. With the as text, Ryan will understand that we only want the string "Ryan".

Setting defaults

If the imported file does not exist or the environment variable is not set, Ryan will, by default, raise an error. You can provide a default value to override this error using or:

import "env:FORGOT_TO_SET" or "Ryan";   // -> "Ryan"
import "does-not-exist.ryan" or {};     // -> (empty dictionary)

The clause or will force the import to use the default value if, for any reason whatsoever the import fails.

Limitations

No dynamic imports

Although import expects a string as input, you cannot use an expression that yields a string; the import must be only a literal string. This will not work:

import "my-" + "file.ryan"

nor will this

let num = 4;
import `file-${num}.ryan`

You can however, go around this limitation in many cases. For example, you can use if ... then ... else ... for conditional imports:

if 1 == 1 then
    import "this.ryan"
else
    import "that.ryan"

Even though import does not accept expressions, it can be freely used within expressions to allow for some level of customization.

No circular imports

Circular imports will result in a nasty, smelly error:

// tweedledee.ryan:
import "tweedledum.ryan"

// tweedledum.ryan
import "tweedledee.ryan"

If you ever find yourself in this situation, you will need to restructure your files in order to destroy the cyclic dependency. If a and b depend on each other, you can to put the "depended" part in a third file c and make both a and b depend on this file instead. This "third file trick" solves most, if not all, situations you might encounter.

Pattern matching

In this chapter, we will talk about the joys of breaking stuff up to its most basic components.

Destructuring bindings

Up to now, we have only worked with variable declarations (bindings) of the garden-variety format:

let a_number = 123;
let a_text = "abc";

However, Ryan allows you to destructure the value after the = sign and bind more than one value to the destructured value. Think of destructuring as circulating parts of a value with a pen and giving the circulated parts names, like so:

let [a, b, ..] = [1, 2, 3, 4, 5];

With this statement, a will be sat to 1 and b will be set to 2. The .. matches the tail of the list.

Here is a list of the simple pattern matches you can do in Ryan:

let _ = "abc";              // wildcard: accepts anything and binds no variables
let x = 1;                  // identifier: matches a variable to a value
let 1 = 1;                  // matches a literal (and binds no variable)

let [a, b] = [1, 2]         // list match: matches all the elements of the list.
let [a, b, ..] = [1, 2, 3]; // head match: matched the first elements of a list.
let [.., a, b] = [1, 2, 3]; // tail match: matched the last elements of a list.

let {"a": b} = {"a": 1};    // strict dict match: matches all values of a dict (b = 1)
let { a } = { a: 1 };       // you can bind a key to a value directly too! (a = 1)
let { a, ..} = {"a":1,"b":2}// dict match: matches only the specified keys

Of course, if the pattern you specified cannot match the input value, you will get an error:

let { a, b } = [1, 2, 3];   // boom!

However, the real fun of destructuring patterns is that they are recursive: you can mix and match them like russian dolls. Therefore, something like this is perfectly legal:

let {
    a,
    b: [c, d, 3],
    "e": _,
    f: { g, h, ..}
} = {
    a: "jalad and darmok",
    b: [1, 2, 3],
    e: "how many lights?",
    f: { g: 4, h: 5, i: 6 },
}

and will create the variables a = "jalad and darmok", c = 1, d = 2, g = 4 and h = 5 all in one go. You can think of destructuring as an alternative and visual way of accessing values from lists and dictionaries.

Pattern matches

Pattern matches power the patterns whe have just presented to create dependent execution, what folks in other languages call functions. A pattern match is a piece of Ryan code that can be applied to a value in order to produce another one. For example:

let foo x = x + 1;
[foo 1, foo 2, foo 3]      // same as [1 + 1, 1 + 2, 1 + 3]

This code will evaluate to [2, 3, 4]. The pattern foo will substitute x by each of the provided values and evaluate the expression for each specific case.

In Ryan, pattern matches only take one argument as input, as opposed to many languages out there that take more than one. However, this is by no means a limitation, because patterns!

let sum_both [a, b] = a + b;
sum_both [5, 7]     // -> 12

You can use any pattern to declare a pattern match.

Closures

All pattern matches in Ryan are closures. That means that you are free to use variables defined outside the pattern match definition in your return expression:

let object_name = "lights";
let there_are quantity = `There are ${quantity} ${object_name}`;

there_are 4     // -> "There are 4 lights"

Locals

A pattern match does not expect only an expression, but a whole block. This means that the body of a pattern match can be its whole self-contained Ryan program, with its own local variables, imports, pattern matches, etc...

let there_are quantity = 
    let object_name = "lights";
    `There are ${quantity} ${object_name}`;

there_are 4     // -> "There are 4 lights"

Patterns are values too

Yep, patterns are values just like any other! Every time you define a pattern match with a let, you create a variable with that pattern match as a value:

let foo x = x + 1;
foo     // -> ![pattern foo x]

You can even make a pattern match be the return value of another pattern match:

let add a = 
    let do_add b = a + b;
    do_add;

(add 3) 2       // -> 5

The parentheses are needed here because pattern match application is left-associative in Ryan.

The only limitation this equivalence is that pattern matches are not representable. Since they don't have a JSON equivalent, they cannot be converted to JSON. If the outcome of your Ryan program contains a pattern match anywhere, you will get an error.

Alternative patterns

The same pattern match can be defined multiple times with different patterns. Ryan will try to match the pattern in order until a match is found and execute the expression associated with the match:

let foo 1 = 2;
let foo 2 = 10;
let foo x = x + 10;

[foo 1, foo 2, foo 3]   // -> [1, 10, 13]

This is very handy when defining special cases and can be used as a more visual alternative to if ... then ... else ....

Recursion is not allowed, in any case!

A pattern match cannot call itself in its code. This will not work:

let foo x = [foo x];
foo 1

This would be an infinite program, that would never end! Thankfully, Ryan will complain that it cannot use the variable foo because it has not been declared before. Even if you try to declare it before, using alternative patterns, it will still not recurse:

let foo [1] = 1;
let foo x = [foo [x]];    // "Now, `foo` is defined", says Will E. Coyote

foo 1   // -> [1]

As you can see, the captured version of foo is different from the version we called in the end. Only the alternatives that existed up to the point of the pattern definition are captured.

Even though recursion is a nice clever trick without which we could not have computers as we know them, it would make Ryan too general for what it was initially conceived: make nice configuration files. It's not expected that people create enormously complex and sneaky algorithms in Ryan. Therefore, to force keeping things simple, no recursion allowed!

Types

Up to now, we have talked about different types of values in Ryan: integers, boolean, string, dictionaries, etc... All these value types have their own representation in Ryan. Even though Ryan is not a statically typed language, you can use types (and expressions over types) to add extra checking to your Ryan code, ensuring that it runs correctly. Furthermore, you can use type guards to further refine patterns, either making them more strict or also allowing them to react differently for different types of data (also known as polymorphism). In this chapter, we will explore what Ryan can offer you in terms of typing.

Primitive types

These are the primitive types in Ryan (you have encountered all them before; we are just giving them Ryan names):

  • bool: booleans; can only be true or false.
  • int: an integer, like 123 or -4, but not fractional numbers such as 1.2.
  • float: a floating point like 1.23 or 6e23. This also includes 1.0, which, although integer, is stored and processed as a float.
  • number: int or float. Includes 123, 1, 1.0 and all other numerical stuff.
  • text: strings of text, such as "Ryan".
  • null: the value null. Only null is of type null.
  • any: anything goes!

Composite types

With lists and dictionaries, Ryan gives you a bit more of flexibility than ony a set of flavours. The most simple types are "collections of the same kind of stuff":

  • [T] (where T is another type): a list where all elements are of type T. E.g., [int] is "a list of integer numbers".
  • {T} (where T is another type): a dictionary where all values are of type T. E.g., {text} is "a dictionary where the values are text".
  • ?T (where T is another type): either something of type T or null. This is called an optional type. E.g., ?float is "either a float number or 'nothing', i.e., null".

However, in a manner similar to patterns, you can also specify the nature of the elements:

  • [A, B, C] (where A, B and C are other types): a list of exactly the specified types (also called a tuple). E.g., [int, bool] is "a list of two elements where the first is an integer and the other is a boolean.
  • {a: A, "b": B} (where A and B are other types): a dictionary with exactly the specified keys whose values correspond to the specified types. E.g., {a: int, b: bool} is "a dictionary with exactly the "a" and "b" keys where the value for "a" is an integer and the value for "b" is a boolean.
  • {a: A, "b": B, ..} (where A and B are other types): a dictionary with at least the specified keys whose values correspond to the specified types. E.g., {a: int, b: bool} is "a dictionary with at least the "a" and "b" keys where the value for "a" is an integer and the value for "b" is a boolean.

Alternative types

In addition to the types we have already seen, you can further compose types with the alternative operator |. The expression A | B means "something that can be either of type A or of type B". In fact, we have already found two syntax sugars for this operation in the previous sections:

  • number is the same thing as int | float.
  • ?T is the same thing as T | null.

However, you can create your own alternative types, e.g.:

  • [int | text]: a list where the elements can be either integer or text.
  • int | {bool}: an integer or a dictionary of booleans.

Type guards

Type guards are an element of the Ryan pattern matching system that will only accept the pattern if, when binding a variable to a value, the value is of the specified type. Type guards are defined with :, like so:

let x: int = 1;     // Success! `x` will be equal to 1
let x: int = "1";   // Error! Expected an integer, got text.

Every time you declare a new variable in a pattern match, you can define an optional type guard. If none is provided, it's assumed that the type is any, i.e., anything goes.

Things get fun when you can bring polymorphism to your pattern matches by powering type guards with alternative patterns:

let foo x: int = `I am an integer: ${x}`;
let foo x: float = `I am a float: ${x}`;
[foo 1, foo 1.0]        // -> ["I am an integer: 1", "I am a float: 1"]

It's recommended that you use type guards wherever possible. It helps keeping your code more explicit on what is going on. Besides, it is one extra way to check the data your program is receiving. For example, suppose you want to set a debug level for your program, which is a number, like:

  1. Only log errors
  2. Log errors and high-level information
  3. Log every small detail in the code execution (also known as verbose). You can validate the input from an environment variable, like so:
let debug_level: int = import "env:DEBUG_LEVEL";

This disallows people from passing DEBUG_LEVEL=off to your program and get a valid configuration, which could save you lots of pain down the line.

Type aliases

Finding yourself writing the same long type over and over again? Fear not! Ryan supports type aliases. Type aliases are variable bindings that associate a variable to a given type. These are a bit different from the regular bindings in which they do not allow destructuring with patterns and they must start with the type keyword, like so:

type X = { a: int, very, text, long: {int}, type_expression: null };

Using regular let biding wont work, because in Ryan type expression are different from regular value expression (and they don't mix!):

let X = int;    // -> error! Expected expression block, but got a type :(...

After you have defined a type alias, you can use it normally as if it were any other type:

type X = int;
let x: X = 1;   // -> ok!

Remember that these are only type aliases. Type aliases do not declare a new type. Therefore, a same variable can conform to many different type aliases at the same type.

Types are not representable

As you can expect, types have no equivalent in JSON. Therefore, even though types are values, if you ever sneak a Ryan type into a value to be represented in JSON, you will get a "not representable" error. By now, the only way to trigger this error is through the misuse of type aliases:

type X = int;
{
    a_type: X,      // -> Un-representable value: int
}

Comprehensions

Comprehensions are a special kind of list and dictionary syntax that lets you make transformations on collections. This includes mapping and filtering values from these collections. If you are familiar with Python list comprehensions, you will feel right at home, since the syntax is almost identical. If not, think of comprehensions as a controlled form of a for loop that looks like a set definition from mathematics.

List comprehensions

A (basic) list comprehension takes the following form:

[<expression> for <pattern> in <expression>]

For example, let's generate a list of even numbers:

[2 * i for i in range [1, 10]]

This will evaluate to [2, 4, 6, 8, 10, 12, 14, 16, 18]. The expression range [1, 10] will generate all integer numbers from 1 to the predecessor of 10 (i.e., 9). If you find this strange, this is the default in most programming languages. Ryan is is not the exception. The comprehension expression binds the variable i to each element from the collection supplied after in and calculates 2 * i for each value.

You can also supply an optional if guard that will filter the elements that will make to the final collection. For example, to get the same result as before, instead of multiplying by 2, we could iterate through all numbers and check the ones that are divisible by 2, like so:

[i for i in range[1, 20] if i % 2 == 0]

This wil also yield [2, 4, 6, 8, 10, 12, 14, 16, 18] as the output.

Dictionary comprehensions

Dictionary comprehensions are very similar to list comprehensions, the only difference being that you also get to set the keys od the dictionary as part of the comprehension:

{<key expression>: <value expression> for <pattern> in <expression>}

So, for example, we could get a mapping from a number to its double, like so:

{fmt i: 2 * i for i in range [1, 10]}

This will yield the dictionary {"1": 2, "2": 4, "3": 6, "4": 8, "5": 10, "6": 12, "7": 14, "8": 16, "9": 18}. Similarly to list comprehensions, you can also supply an optional if guard to filter the values:

{ fmt i / 2: i for i in range[1, 20] if i % 2 == 0 }

This will yield the same dictionary as before.

What can go after a for ... in

Things that can go after the in keyword (also called iterables) are by now only lists and dictionaries. In the case of dictionaries, the patter in the for will be matched to the tuples of keys and values in the dictionaries, like so:

{ y: x for [x, y] in {"a": "b", "c": "d"} }

This will yield the value {"b": "a", "c": "d"} as a result.

As you can see, there are also some handy patterns that can help you with some usual iterating tasks. We have already encountered range, that returns lists of consecutive numbers, but there are three more useful patterns that always come in handy:

  • enumerate: returns pairs of the index of an element and the element of the iterable, like so:
enumerate [1, 4, 6, 9]      // -> [[0, 1], [1, 4], [2, 6], [3, 9]]
  • zip: walks through a list of iterables in lockstep, like so:
zip [[1, 2, 3], [4, 5, 6]]  // -> [[1, 4], [2, 5], [3, 6]]
  • sort: returns a sorted version of a list:
sort [1, 4, 3, 2]       // -> [1, 2, 3, 4]

The Ryan CLI

The simplest way of interfacing with Ryan is through the Ryan CLI. The CLI is an app that executes your Ryan programs and spits out the final computed JSON either to a file or to the terminal. Since the CLI is a program that you can download in most computers, you can use it independently of your programming environment. In fact, if your language does not support native integration, don't fret: the CLI is purpose built for integration with other programs and applications.

Simple usage

Evaluating a Ryan program is as simple as writing:

ryan my_program.ryan

This will execute a file my_program.ryan, stored in the current working directory, and print the returned JSON on the screen, with indentations, pretty colors and other niceties. Else, it will print an error message on the screen, saying, more or less cryptically, what is wrong with your code. In case the code executes to valid JSON, the return status will be 0, else, if there is any problem, the return status will be non-zero (the actual value depends on the error). This is in line with all well behaved programs and your computer will have no problem understanding it.

If you wish to save the calculated value to a file, just use the > operator:

ryan my_program.ryan > output.json

In the same vein, you can set environment variables as usual, which (for Linux and MacOS) is:

LIGHTS=4 SHAKA="when the walls fell" ryan py_program.ryan

Or...

export LIGHTS=4
export SHAKA="when the walls fell"
ryan py_program.ryan

Getting help

If you want to dig deeper into the CLI, you can use the --help command, like so:

ryan --help

This will print useful information on the available commands and options along with the current version of Ryan that you are using.

Integrating into your program

If your language does not support Ryan natively yet, or if you don't wish to use the existing native support, you can use the Ryan CLI to generate the final JSON for you. All you need to do is:

  • Spawn a subprocess ryan <file> and capture its output, more specifically, its stdout.
  • Check if the return status is 0. This will indicate if any error happened.
  • Decode de output using you languages support for JSON. The output is guaranteed to be valid.

All these steps are standard to most, if not all, modern programming languages and you should be able to easily implement them without any external library or resources.

Rust

To get ryan into your Rust project, just add the crate ryan with cargo:

cargo add ryan

Since Ryan is natively written in Rust, this crate is fully featured. In fact, this is the same crate used to create the Ryan CLI.

For information on how to use Ryan in Rust, see the docs. All relevant details are explained in length there and it would not be expedient to just repeat everything in this tutorial.

Python

Installing ryan

You can install Ryan directly from PyPI using the ryan-lang package:

pip install ryan-lang

With this command, you will get the ryan Python module at its most recent version installed in your environment.

Using ryan

To use ryan, you just need to import it and use the from_str method:

import ryan

some_values = ryan.from_str(
    """
    let x = "a value";
    { x, y: "other value" }
    """
)

Or you can read a JSON value directly from a file using from_path:

import ryan

some_values = ryan.from_path("some_values.ryan")

Current limitations

The python library currently only exposes functions powering basic usage. This means that more advanced features, such as custom native patter matches and custom importers are not supported. However, these are more advanced features that most people will not need to use (and are not even covered in this tutorial). Most likely, the current exposed features will suffice for your use case. This limitation is intended change in a future version of this library.

JavaScript

There are two ways you can use Ryan with JavaScript: you can use Ryan in a Web environment or you can use Ryan with NodeJS in your computer.

Ryan on the Web

To install Ryan for a Web environment, you will need to integrate it with a bundler, such as WebPack. To do so, just add the ryan-lang-web package to your project:

npm install ryan-lang-web

From there, you can import Ryan into your project, like so:

import * as ryan from "ryan-lang-web";

var result = ryan.fromStr(`
    let x = "a value";
    
    {
        x,
        y: "other value" 
    }
`);

// result will be `{ "x": "a value", "y": "other value"}`

Since the Web doesn't have a filesystem nor environment variables, the import system for Ryan works differently in the browser than in other environments. Basically, if nothing is set, Ryan will run in hermetic mode, i.e. with all imports disabled. However, a list of imports can also be easily configured. For more information, see the package homepage in the NpmJS.org website.

Ryan with NodeJS

By now, Ryan for NodeJS is a direct port from the Ryan for the Web package, via the magic of WASM. To add Ryan to your NodeJS project, just use npm:

npm install ryan-lang-node

Since this is a direct port, this package works just like described in the last section. Unfortunately, this includes the fact that Ryan for NodeJS does not understands the filesystem or environment variables. This is a limitation that will be resolved in a future release, hopefully sooner than later. This will entail rewriting as a proper native NodeJS extension, running native code, as opposed to running WASM.

List of built-ins

Pattern Description
fmt x: any Transform any object into a string that represents it. Use this pattern to interpolate non-string values with string values in order to create more complex displays, e.g., "there are " + fmt 4 + " lights". Without the fmt, you will get a type error.
len x: [any] | {any} | text Gets the length of a list, a dictionary or a string.
range [start, end] Generates a list of consecutive integer numbers from start to end - 1.
zip [left, right] Iterates through both iterables at the same time, returning a list with the pairs of elements in the same position. For example, zip [[1, 2, 3], [4, 5, 6]] yields [[1, 4], [2, 5], [3, 6]].
enumerate x: [any] | {any} Generates a list of indexed value for a list. For example, enumerate ["a", "b", "c"] yields [[1, "a"], [2, "b"], [3, "c"]].
sum x: [number] Returns the sum of all numbers in a list.
max x: [number] Returns the maximum of all numbers in a list.
min x: [number] Returns the minimum of all numbers in a list.
all x: [bool] Returns true if there is no false in the list.
any x: [bool] Returns false if there is no true in the list.
sort x: [number] | [text] Returns a sorted version of a list.
keys x: {any} Returns the a list of the keys in the dictionary.
values x: {any} Returns the a list of the values in the dictionary.
split sep: text Returns the a pattern that splits a text by the supplied separator. Use it like so: ( split "," ) "a,b,c" = ["a", "b", "c"]
join sep: text Returns the a pattern that joins a list of text with the supplied separator. Use it like so: ( join "," ) ["a", "b", "c"] = "a,b,c"
trim x: text Returns a text with all leading and trailing whitespaces removed.
trim_start x: text Returns a text with all leading whitespaces removed.
trim_end x: text Returns a text with all trailing whitespaces removed.
starts_with prefix: text Returns a pattern that tests if a text starts with the given prefix. Use it like so: ( starts_with "foo" ) "foobar" = true
ends_with postfix: text Returns a pattern that tests if a text ends with the given postfix. Use it like so: ( ends_with "bar" ) "foobar" = true
lowercase x: text Makes all letters lowercase.
uppercase x: text Makes all letters uppercase.
replace [find: text, subst: text] Returns a pattern that substitutes all occurrences of the text find with the text subst. Use it like so: ( replace [ "five", "four" ] ) "There are five lights" = "There are four lights"
parse_int x: text Parses some text as int, e.gparse_int "123" = 123. This raises an error if the text is not a valid integer.
parse_float x: text Parses some text as float, e.gparse_float "123" = 123.0. This raises an error if the text is not a valid float.
floor x: float Calculates the floor of a given number.
ceil x: float Calculates the ceiling of a given number.
round x: float Rounds a given number to the nearest integer.