Raga
// one raga. every rendition.
Raga is a small, explicit language built from one shape. A variable, a function, a type, a class, a loop and a call with a block all follow one pattern.
The language is described by examples written in Raga itself. Start with the templates, they explain one area each, in order.
[[attributes]]
[ const | let | Type ] name = ( head [(args)] [|params|] [Type] [{ body }] )+
If you can read that line, you can read Raga.
01The one shape
Every declaration has the same parts, in the same order. Learn the parts once and the whole language reads the same way.
| Part | Meaning |
|---|---|
| [attributes] | Facts about the declaration, written on the line above it. Optional. |
| const / let / Type | How the name is bound. Exactly one of the three, never a type together with const or let. const cannot be rebound, let can, a type first picks the type: u8 level = 5. |
| name | What you call it. |
| = | Binds the name to what follows. |
| head | What is being made or done: a value, a type name, function, method, struct, enum, union, class, interface, trait, attribute, get, or a call. |
| (args) | The values that go in: parameters of a function, arguments of a creation, the condition of an if. |
| |params| | The names that the head hands to its block: for (items) |item| { }, catch |e| { }. |
| Type | The type of the result. |
| { body } | The code, or the members. |
| ( … )+ | The chain: the same parts again, with a head of its own, on the line of the closing }. |
The same shape, in different clothes
const limit = 100 // a value
u8 level = 255 // a value, with its type chosen first
const area = function(f64 radius) f64 { // a function
return(3.14159 * radius * radius)
}
const Point = struct { f64 x; f64 y } // a type
const Color = enum { const Red = 0; const Green = 1; const Blue = 2 } // a type
const route = attribute(String path) // an attribute
Without a name, it is a statement
A condition, a loop and a call with a block are the same line without the binding: head (args) |params| { body }, repeated as often as needed.
if (temperature > 30) {
weather = "hot"
} elif (temperature > 20) {
weather = "warm"
} else {
weather = "cold"
}
try {
check_age(200)
} catch |e| { // the next clause starts on the line of the }
print(e.message)
} finally {
cleanup()
}
02Philosophy
Raga is small on purpose. Each idea follows from the shape, and nothing is done behind your back.
Everything is a binding
A function, a type and an attribute are values bound to a name, declared like any variable: const area = function(…), const Point = struct { }. There are no special declaration forms to learn.
One bracket, one meaning
( ) values, < > types, { } a body or the members, | | names handed to a block, [ ] attributes on the line above a declaration.
Nothing is hidden
No implicit conversion, no hidden return, no hidden this, no null, no overloading of plain functions. return is a call, return(value) or return(), always written. A name must be declared above its use.
The name tells the kind
A primitive (i32, f64, bool, lowercase) is a plain value that is copied. Everything else (String, List, Function, every struct and class, Capitalized) is a value too: assignment copies it, with everything inside. There is no clone. To share, point at it with addr(x). const protects the whole value.
Failure is part of the type
A call that can fail returns Optional<T> or Result<T, E> and is read with match. What should never happen is thrown with throw(Error(…)) and handled with try.
Memory is scoped, not counted
A value is freed when the block of its name ends, so there are no cycles and no pauses. A pointer, p.*, never keeps anything alive. alloc and free exist for the rare case that needs manual memory.
Facts go above the declaration
An attribute is written in [ ] on the line above. [page("/")] is data your code can read, [method(Ownership.Static)] changes what a method is. Modifiers are members of enums, not reserved words.
One file, one module
export { a, b } ends a file and import("./path") reads another. There is no main function: the file you run goes from the first line to the last.
Conventions
| Types, enum members, variants | Capitalized |
| Variables, parameters, fields, functions, attributes | lower_snake_case |
| Methods and fields of a class, interface or trait | lowerCamelCase |
| Layout | A clause starts on the line of the } before it. match and switch hold one case per line. |
03Language features
The ideas that follow from the shape, each shown in a few lines.
Types in angle brackets, values in parentheses
List<i32> and Map<String, i32> name a type. area(2.0), Point(x = 1.0, y = 2.0) and u8(300) take values. A cast is a type name called on a value. Collections have literals that read like their constructors: "text", {1, 2, 3} (Array), [1, 2, 3] (List), (1, 2.5) (Tuple).
Functions are values, and closures
A function can be declared anywhere, uses the names around it, and can return another function. What a closure uses moves to the heap and lives as long as the function does.
const make_adder = function(i32 step) Function<[i32], i32> {
const add = function(i32 n) i32 { return(n + step) }
return(add)
}
const add_two = make_adder(2)
const seven = add_two(5)
Trailing blocks
The last argument of a call can be written as a block after the parentheses. It is a function body for a callback, the values of an Array, or the name = value lines of a struct, and the type of the last parameter says which. When the block is the only argument, the parentheses are left out. A name without parentheses is the function itself, never a call: double is the function, double(5) calls it.
const apply = function(i32 value, Function<[i32], i32> transform) i32 {
return(transform(value)) // the last parameter is a callback
}
const double = function(i32 n) i32 { return(n * 2) }
const doubled = apply(4) |n| { return(n * 2) } // the block fills the last parameter
const ten = double(5) // a call always has parentheses
const Column = struct {
String name
i32 size
}
const table = function(String name, Column column) { }
const tally = function(String title, Array<i32> numbers) i32 { return(numbers.len()) }
table("users") { // the struct is the block...
name = "id"
size = 4
}
const total = tally("scores") { 4, 8, 15 } // 3, the Array is the block
Chains of blocks, built with structs
Calls that return values are chained with a dot, user().select().limit(1). When the last argument of a call is written as a block (a callback, an Array or a struct), a dot after the closing brace would look strange, so the dot is left out and the next name follows the brace: user { } select { } limit (1).
For your own code a function takes one trailing block, for its last parameter, so a chain of blocks is built with structs: each step returns a struct that has only the methods which may come next. The return type of a step decides which clause can follow.
when(a > 3) {
print("big")
} or_when(a > 1) {
print("small")
} otherwise {
print("none")
}
Classes, with the same shape inside
A field is a variable, a method is a binding whose head is method, and overloads and accessors are chains on the closing brace.
[class(Inheritance.Final)]
const Student = class {
String name = ""
const greet = method() String {
return(this.name)
} method(String greeting) String { // an overload is a chain
return(String.join(greeting, this.name))
}
const marks = get { return(this._marks) } set |value| { this._marks = value }
} extends(People) implements(ISpeak)
Failure is in the type
const half = function(i32 n) Result<i32, String> {
if (n % 2 != 0) { return(Result.Err("odd")) }
return(Result.Ok(n / 2))
}
match (half(10)) {
case (Ok) |ok| { print(ok.value) }
case (Err) |bad| { print(bad.reason) }
}
Attributes, in three patterns
You declare your own attributes with the head attribute. They build on each other: metadata that code can read, a hook that runs once while building, and a hook that runs on every call. The block names everything it uses between its | |, so nothing arrives under a hidden name.
const metadata = attribute(String name) // 1. metadata only
const routes = Map<String, BindingInfo>()
const route = attribute(String path) |target, path| { // 2. a build hook
routes.set(path, target)
} call |target, args, next| { // 3. a call hook
print(String.join("calling ", target.name))
return(next(args))
}
[route("users/")]
const users = function(i32 first, i32 count) String { return("list") }
Build-time evaluation and reflection
compile(expression) runs ordinary code while the program is built and puts the result in the program as a constant. reflect<T>() describes a type as data, and attributes_of reads the attributes on a declaration.
const square = function(i32 n) i32 { return(n * n) }
const sixty_four = compile(square(8)) // computed by the compiler
const info = reflect<Account>()
const field_count = info.fields.len()
Modules
const shapes = import("./geometry/shapes") // a file
const check = import("test") // a library by name, resolved through raga.project and the libraries folder
export { area, perimeter } // the last statement of a file
What Raga does not have
- No null
- Nothing to throw but an
Error - No implicit casts
- No overloading of plain functions
- No inheritance for structs
- No multiple inheritance
- No operator overloading, except with the
[operator]attribute - No main function
04Templates
Start with hello.raga for the whole language on one page, then read the templates in order. Each one explains one area in full, and none uses a feature that a later one explains.
- hello.ragathe whole language in one page
- 01-basicsvariables, primitives, literals, operators, scope
- 02-data-structuresArray, List, Tuple, Set, Map
- 03-control-flowif, switch, match, loops
- 04-functionsparameters, callbacks, trailing blocks, closures
- 05-structs-enums-unionsyour own types, Optional and Result, chaining
- 06-errorsOptional, Result, throw, try
- 07-classes-and-traitsclasses, inheritance, interfaces, traits
- 08-memorycopying, pointers, ownership, manual memory
- 09-attributes-and-reflectionattributes, build and call hooks, reflection, compile
- 10-modulesimport and export, libraries
- 11-standard-librarythe built-in functions and methods
Editor support
The VS Code extension gives syntax highlighting, completion, hover, go to definition, snippets and errors as you type, including the trailing block and the next clause of a chain. Install it from the Visual Studio Marketplace, or read the source in vs-code-extension.