This document outlines the design for the first version of rapt (v0.1.0). It is designed as a modern functional programming language, aiming combine the best features of other languages, such as Gleam, Erlang, Rust, and Zig. It will be a robust general-purpose language that is a highly-scalable, safe, and maintainable environment for developers. Rapt will be implemented in Zig, leveraging its high performance and low-level control.
Rapt embraces many language paradigms: functional programming with immutability at its core; elements of concurrent programming through built-in support for lightweight processes and messaging; elements of object-oriented programming in its module system and type design, although many traditional features like inheritance/polymorphism are not included; and declarative programming.
This document aims to outline the design of rapt and serve as a comprehensive guide for a future implementation. As of now, no code has been written, and this design is subject to extreme change based on challenges in implementation, feedback, and what aligns with the goals of this language.
Functions are first-class citizens in rapt, meaning they can be assigned to
variables, passed as arguments, and returned from other functions. They are
defined with the fn
keyword and use =
to separate the signature from its
body, similar to how most languages would define their variables.
fn add(a: Int, b: Int) = a + b
Functions with multiple expressions would use a block:
fn complex_calculation(a: Int, b: Int) = {
let c = x * y
let d = c + 10
d * 2
}
Anonymous functions (lambdas) use a similar syntax, just without the fn
keyword:
let multiply = (a: Int, b: Int) = a *b
Functions can take other functions as arguments:
fn apply_twice(fun: (Int) -> Int, a: Int) = fun(fun(a))
They can also return a function:
fn create_multiplier(factor: Int) = (a: Int) = a * factor
Rapt uses let
for rebindable variables and const
for constants set at the
top level. All variables are immutable by default, promoting safer and more
predictable code by preventing unintended modifications.
let a = 5 // Not allowed; compile-time error due to `let` at the top level
const b = 5 // Allowed; b = 5
const b = b + 5 // Not allowed; compile-time error due to rebinding constant
fn foo() = {
let a = 5 // Allowed; a = 5
let a = a + 5 // Allowed; a = 10
const b = 5 // Not allowed; compile-time error due to `const` not at the top level
}
Rapt has a strong static type system with inference. Types are defined using
the type
keyword.
type Name = String
type Age = Int
type IntPair = (Int, Int)
type StringList = List(String)
type Pair(a, b) = {
first: a,
second: b,
}
type User = {
name: String,
age: Int,
}
type Option(a) = Some(a) | None
type Result(value, error) = Ok(value) | Error(error)
type Shape =
| Circle(radius: Int)
| Rectangle(width: Int, height: Int)
| Triangle(base: Int, height: Int)
Pattern matching is the most important, and one of the only, ways to manage control flow in rapt. It can also be used for destructuring.
import std/int
fn describe_point(point: (Int, Int)) = {
case point {
(0, 0) = "Origin"
(0, y) = "On y-axis at " <> int.to_string(y)
(x, 0) = "On x-axis at " <> int.to_string(x)
(x, y) =
"Point at (" <> int.to_string(x) <> ", " <> int.to_string(y) <> ")"
}
}
import std/int
fn process_result(result: Result(Int, String)) = {
case result {
Ok(value) = "Success: " <> int.to_string(value)
Error(msg) = "Error: " <> msg
}
}
Rapt allows for pattern matching assertions by using the assert
keyword:
fn get_username(user: Option(User)) = {
let assert Some(user) = user
user.name
}
fn process_success(input: String) = {
let assert "Success: " <> msg = input
msg
}
Pattern matching assertions allow you to combine pattern matching with variable binding, throwing an error if the pattern doesn't match. This can lead to more concise and expressive code.
The rapt compiler will perform exhaustiveness checking on pattern matches, ensuring that all possible cases are handled:
type TrafficLight = Red | Yellow | Green
fn describe_light(light: TrafficLight) = {
case light {
Red = "Stop"
Yellow = "Caution"
// Not allowed; compile-time error due to green not checked
}
}
Code in rapt is organized into modules. The module name is derived from the
file name, or, if the file is titled mod
, it is taken from the directory
name.
// In `src/std/math/mod.rapt`
pub fn add(a: Int, b: Int) = a + b
pub fn multiply(a: Int, b: Int) = a * b
fn private_helper() = {
// not visible outside the module
}
import std/math
fn calc(x: Int, y: Int) = math.add(x, math.multiply(y, 2))
You can also import specific functions:
import std/math.{add, multiply}
fn calculate(x: Int, y: Int) = add(x, multiply(y, 2))
Rapt supports Erlang-style concurrency, meaning lightweight processes and message passing.
import core/process
type Message =
| Incr
| Get(pid: process.Pid)
fn spawn_counter() = {
process.spawn(() = counter(0))
}
fn counter(count: Int) = {
case process.receive() {
Incr = counter(count + 1)
Get(pid) = {
process.send(pid, count)
counter(count)
}
}
}
let pid = spawn_counter()
process.send(pid, Incr)
process.send(pid, Get(process.self))
let count = process.receive() // 1
Rapt uses the Result
type for error handling, encouraging explicit error
management.
import std/int
// returns Result(Int, String)
fn divide(a: Int, b: Int) = {
case b {
0 = Error("Division by zero")
_ = Ok(a / b)
}
}
fn use_division = {
case divide(10, 2) {
Ok(result) = "Success: " <> int.to_string(result)
Error(msg) = "Error: " <> msg
}
}
- Identifiers:
[a-z][a-zA-Z0-9_]*
for variables and functions,[A-Z][a-zA-Z0-9_]*
for types and modules. - Keywords:
const
,let
,type
,fn
,case
,import
,pub
- Operators:
+
,-
,*
,/
,=
,==
,!=
,<
,>
,<=
,>=
,<>
(string concatenation),|>
(piping)
Continuing on that, rapt supports arbitrary-depth exponentiation operations. The snytax is as follows:
**
: Standard exponentiation (power)***
: Tetration****
: Pentation- etc (no upper limit)