Quickstart
This version of the documentation is intended for everyone who is already familiar with the practice of programming in general and has previously worked with other languages. In case you don't have any experience in programming yet, you should give the User Documentation a try, which is meant for beginners and does not require any prior knowledge. Please note that this documentation is intentionally kept concise and only briefly introduces each aspect of the language.
Getting started
Rouge requires zero setup when the web-based IDE Rouge Studio is used. The usage of the IDE should be self-explanatory to anyone who has used any other IDE, editor, or REPL.
Basic design
Rouge is an educational programming language, which means that all design decisions were made with the goal to improve the learning experience for beginners. The language relies on automatic memory management (implemented via garbage collection), meaning the user does not have to manually (de-)allocate memory. The type system uses strong and static typing, meaning that type safety is applied strictly and before runtime. In its default implementation, Rouge is executed by an interpreter that can be run cross-platform, even on the web. Syntactically, Rouge behaves very similarly to many C-style languages, such as TypeScript, Kotlin, or Dart. If you are familiar with one of those or a similar language, most aspects of the Rouge language will feel familiar.
Comments
The language features C-style line as well as block comments. The interpreter ignores all text inside a comment.
Variables and fundamental data types
Variables are declared without a dedicated keyword, such as let or const. Explicit type annotations are possible in Rouge; however, type inference makes them unnecessary most of the time.
The language offers three so-called fundamental data types. They are somewhat similar to what is often referred to as primitive types in other languages, because all other, more complex types, comprise them. However, primitive types are often limited in their abilities, whereas Rouge's fundamental types initialize to fully featured objects. They are part of the standard library and don't have to be imported. You can create values of these fundamental types by spelling out their type literals.
- String
- name: String = "Steve"
- Boolean
The possible values are
trueandfalse.name: Boolean = false- Number
Represents all real numbers.
name: Number = 42 fraction: Number = 3.99
Numeric expressions
The language offers the following numeric operators
Operator | Description |
|---|---|
| Addition |
| Subtraction |
| Division |
| Multiplication |
| Modulo |
In terms of operator precedence, Rouge follows the "dot before line" rule. When multiple dot or line operators are used in isolation, they are evaluated from left to right. To override this order of execution, parentheses can be used.
Boolean expressions
Values of the same type can be compared using the following set of boolean operators.
Operator | Description |
|---|---|
| Equal to |
| Not equal to |
Additionally, numbers can be compared using the following operators.
Operator | Description |
|---|---|
| Smaller than |
| Smaller than or equal to |
| Greater than |
| Greater than or equal to |
By applying these operators, a boolean expression is created, which evaluates to a boolean value. Multiple boolean values/expressions can be combined using the following set of operators.
Operator | Description |
|---|---|
| And |
| Or |
| Negation |
Boolean operators also follow certain rules when it comes to operator precedence. To begin with, the combination operators (&&, ||) take action. Second, the comparison operators (==, !=, <, <=, >, >=) are evaluated from left to right. Just like with numeric expressions, parentheses can be used to override the order in which boolean expressions are evaluated.
Functions
Functions are first-class citizens in the language, meaning they are values that can be passed around. The only way to define functions is by using an anonymous function definition. Functions become non-anonymous by being assigned to a variable.
Function parameters always need to specify their type. When the function is called, parameters are passed positionally. Functions can specify a return type via the arrow syntax (->). When a return type is specified, the function needs to return a value via the return keyword. In case no return value is specified (by omitting the -> Number section from the example), the function implicitly returns an instance of the Nothing type from the standard library. Only if the function omits the return type annotation or explicitly returns Nothing, no return statement is required.
User defined types
It is possible to build custom types using the type keyword. Each type has a set of attributes (referred to as fields) for each of which we need to specify a unique name as well as its type. It should be mentioned that field names only have to be unique in the namespace of the type. Each type definition results in the generation of a constructor function, which allows us to create an instance of the type.
Properties of a type instance can be accessed via the dot-notation.
Methods
You can specify default values for the fields in type definitions. When a field has a default value, the value can no longer be initialized in the constructor. If you wish to modify the value of the field, you should do so by manually overwriting the attribute later. Also, fields with default values have to be specified at the end of the type definition.
The fact that it is possible to specify default values for fields can be used to define methods on the type. For instance, the following example includes the method called is_adult, which checks whether the age of the Person is at least 18.
As you can see, a method is a function assigned to a field via the default value syntax. Calling the method is a mix of the dot notation for property access as well as the function invocation syntax.
Inside the method, other attributes of the type can be accessed via the this binding. Calling the this parameter "this" is only a convention and therefore not required, but still recommended for consistency. Technically, the language only checks whether the first parameter of a method is of the same type as the surrounding type. If so, the language will automatically pass the instance to the method. In fact, the language does not allow you to pass the instance to the method yourself. If you define a method without a this parameter of the correct type, you can still call the method, however, you will be unable to access any other field of the instance. Apart from these differences, methods behave exactly the same as any other function.
Placeholders
The language allows you to generalize behavior by creating higher order types as well as functions that accept types, not only values as parameters. This concept is often referred to as Generics, Generic Type Parameters, or Templates in other languages, while Rouge refers to them as Placeholders.
When defining a higher order type, placeholders are specified using the angled brackets syntax. For instance, in the following example, the type called Wrapper can wrap values of any type because the type of the wrapped value can be passed in. A placeholder called T is established, which can be used inside the scope of the type definition. At the time of instantiation, you are required to explicitly pass types to fill the placeholders, also using angled brackets. In the example, T is set to String, meaning that value also becomes a String. For each instantiation of Wrapper, its placeholders can be set individually.
Functions can also accept placeholders using the same syntax. You are also required to explicitly pass a type to each placeholder that the function specifies.
Writing to the console
To print to the standard output, you can use the print function provided by the standard library. The function accepts a String as input, which is printed to the console with a trailing newline. Should you wish to not have a newline included at the end, you can rely on print_no_newline, which is also available in the standard library. These functions are available everywhere and do not need to be included or imported.
To print values other than of type String, you can make use of string interpolation which allows you to embed values into strings. The newly created string can then be passed to print. String interpolation only supports fundamental data types, however. If you wish to print complex types to the console, you may need to set up your own serialization routine, breaking down the type into fundamental values that can be printed individually.
At this point, it is important to mention that string literals support escape sequences. For instance, if you desired to circumvent string interpolation and instead print the "raw" string, you could prefix the interpolation with a backslash.
Output:
Traits
Rouge provides functionality to standardize behavior between types. While other languages include dedicated constructs, such as interfaces, Rouge does not. Instead, types can implement other types. Instead of defining an interface, you will define another type. Take a look at the following example where there is a parent type called Animal that is implemented by Dog and Cat.
A type implements another type by using the implements keyword. When Dog implements Animal, Animal is referred to as a trait of Dog. Dog is required to implement all fields of Animal. As you can see, make_sound defines a this parameter of type Animal. However, in Dog, you are allowed to set this to Dog instead of Animal, because Dog is a subtype of Animal. This enables you to access all other fields defined by Dog.
Traits are Rouge's implementation of polymorphism, meaning any function or type that accepts an instance of Animal can also accept an Instance of Cat or Dog.
A type can implement multiple traits if the traits don't define any conflicting behavior. Also, Traits can make use of placeholders.
Null safety
Rouge implements null safety, meaning you cannot set variables or parameters to null like you can in many other languages. However, you may still encounter situations where you need to model the absence of a value. For this reason, the standard library provides the Option type.
Option is implemented by Something and Nothing which represent the presence, or absence of a value respectively. The contained value is of type T, a placeholder. Whenever you need to store a value that can be absent, you wrap your value in an instance of Option. The has_value() method allows you to check whether it is safe to access the underlying value. In case has_value() returns true, you can access the value via get_value(), which returns the value in case the Option is a Something. If you forget to verify whether it is safe to access the contained value, and call get_value() on a Nothing, the interpreter will panic with an unrecoverable error.
The Option type and its implementations are supplied by the standard library and can be used without needing to be imported. The advantage of using a type like Option instead of making every value in the language nullable is that it becomes an explicit choice which values you decide to mark as optional. Also, you are forced to check whether an optional value contains a value before accessing it, leading to safer code.
Error handling
Error handling in Rouge works similarly to how the language deals with the absence or presence of values. Instead of being able to throw errors that automatically propagate through the call stack, errors are represented by a monadic type called Result. A Result can either be Ok or an Error, representing that a computation was successful or erroneous, respectively.
The main difference to the Option type is that Result stores a separate value in case of failure. Result also has two placeholders, T and E which define the types of the Ok and Error cases respectively.
The benefit of using a type such as Result for error handling is that it becomes, once again, explicit whether an operation can fail or not. Also, there is the benefit of not introducing further syntax, because Result can entirely be implemented with existing language features and standard library components.
Naming conventions
While Rouge permits all naming conventions when naming types or variables, the standard library, as well as all first-party examples, uses snake_case. Therefore, it makes sense to stick with those rules for your own programs as well. Variables should use the lowercase version (snake_case) while types should use the uppercase/capital (Snake_Case) variant.
Trailing commas
Rouge supports and encourages the use of trailing commas in all aspects of the language; however, it is not required anywhere.