Rho Rho language and toolchain

Types

What a Rho value can be, and how a shape is built out of them.

The types

KindWhat it is
u8 u16 u32 u64Unsigned integers, fixed width
i8 i16 i32 i64Signed integers, fixed width
f32 f64Floating point
booltrue or false
UnitThe type of a function that returns nothing; written void at a signature
Ptr[T]A pointer to T
[T]A slice of T: a view over memory someone else owns
Option[T]A value that may be absent
Result[T, E]A value, or an error
map[K, V]A map, supplied by a port
structA product type, declared in the module
enumA sum type, declared in the module

Integers do not widen implicitly. A u8 is not a u32; the conversion is
written, so a narrowing is always visible in the source.

Option and Result

A value that may be absent is an Option[T], and it is asked about rather than
guessed at:

let led: Option[Ptr[RoboHubGPIO]] = robohub_get_gpio("user_led")
if is_some(led) {
    write(value(led), 0, 1)
}

This is why an absent handle is something a program handles rather than something
it trips over. A registry answering "no such name" is a real answer, and the type
is what makes the program see it.

A function that can fail says so in what it returns:

fn divide(numerator: i32, denominator: i32) Result[i32, string] {
    if denominator == 0 {
        return Err("division by zero")
    }
    return Ok(numerator / denominator)
}

Generics

A declaration may take type parameters, and a parameter may have a default:

fn None[T = Unit]() Option[T] {
    return Option[T] { false, default(T) }
}

None() on its own means an Option[Unit]: no annotation, and no ambiguity
about what "no value" holds.

Generic declarations are templates, and they are instantiated rather than looked
up through a table: every application of a declaration is a concrete function, so
a call is a direct, inlinable call and there is no vtable to indirect through.

Slices

A slice is a pointer and a length. It is a view, not an owner: making one costs
nothing, and what it points at is somebody else's memory.

fn first[T](items: [T]) Option[T] {
    if length(items) == 0 {
        return None()
    }
    return Some(items[0])
}

There is no resizing. A slice that must grow is a slice over a buffer the program
owns, sized when it was declared.

Structs

A struct is a product type, and a value of it is passed whole:

struct Counter {
    value: u32
}

fn increment(c: Counter) Counter {
    return Counter { c.value + 1 }
}

Because there are no globals, a value is passed or reached through a field. That
is what makes a function testable: a counter is an argument, not something a test
has to set up around it.

Enums

An enum is a sum type. Each variant carries what it needs to be told:

enum Shape {
    Circle(f64)
    Rect(f64, f64)
}

fn area(s: Shape) f64 {
    match s {
        Circle(r) => return 3.14159 * r * r
        Rect(w, h) => return w * h
    }
}

A match is exhaustive: a function that matches an enum returns in every arm or
does not compile, so adding a variant is a change every reader of the type sees.

Foreign types

A type a C header declares is a Rho type by name, and the pointer to it is a
Ptr:

extern fn get_gpio(name: string) Option[Ptr[RoboHubGPIO]]

Nothing about the header is written out by hand: rho-cimport generates these declarations from the headers the project
named, so a header that changes changes them with it. An opaque C struct — one
the program may hold but not read — becomes a name the program can pass around and
nothing else, which is the same thing the header's own author intended.

Where to go next

  • syntax — how these are written.
  • stdlib — what the standard library is built from.