Quickstart
Install Baseline, then go from hello world to hello web server.
Installation
brew install baseline-lang/tap/baseline Hello World
Create hello.bl:
@prelude(script)
fn main!() -> {Console} () =
Console.print!("Hello, World!") $ blc run hello.bl
Hello, World! @prelude(script) loads the standard library.
The ! suffix means this function has side effects, and {Console} says which ones.
Functions
Every function is fn name(params) -> Type = body.
The last expression is the return value, there is no return keyword.
If the body is a single expression, write it right after =.
If you need multiple steps, wrap the body in { } braces.
The last expression in a block is the return value.
// Single expression: no braces needed
fn add(a: Int, b: Int) -> Int = a + b
// Still one expression, just on the next line
fn greet(name: String) -> String =
"Hello, ${name}!"
// Multiple steps: use braces
fn process(input: Input) -> Output = {
let parsed = parse(input)
let validated = validate(parsed)
transform(validated)
}
// Lambdas use |args| body
let double = |x| x * 2When Do I Need Type Annotations?
Baseline uses type inference. You must annotate exported and effectful functions, but can omit types on local variables, lambdas, and private helpers.
// Exported: full annotations required
export fn add(a: Int, b: Int) -> Int = a + b
// Private helper: types inferred
fn double(x) = x * 2
// Local variables and lambdas: inferred
let names = List.map(users, |u| u.name) Effect Braces
The curly braces {...} after -> declare which
side effects a function uses. Pure functions don't have them at all.
// Pure: no braces, no ! suffix
fn add(a: Int, b: Int) -> Int = a + b
// Effectful: ! suffix + braces declare effects
fn main!() -> {Console} () =
Console.print!("hello")
// Internal effectful: effects can be inferred
fn log_request!(req) =
Console.print!(req.path) The rule: exported and public functions must declare their effects explicitly.
Private helpers get them inferred automatically. The ! suffix
always tells you a function has side effects, and the braces tell the compiler which ones.
Pipes
|> passes the left side as the first argument to the right
side. You read the data flow left to right instead of inside out.
fn active_names(users: List<User>) -> List<String> =
users
|> List.filter(|u| u.active)
|> List.map(|u| u.name) The compiler warns you (STY_001) if you nest single-argument
calls instead of piping.
Pattern Matching
match is exhaustive. Miss a branch and the compiler tells you.
Destructure variants to pull out data.
type Shape =
| Circle(Int)
| Rect(Int, Int)
fn area(s: Shape) -> Int =
match s
Circle(r) -> 3 * r * r
Rect(w, h) -> w * hError Handling
No exceptions. If a function can fail, it returns Result<T, E>. Use ? to propagate
or match to handle it yourself.
fn parse_port(s: String) -> Result<Int, String> =
let n = Int.parse(s)?
if n >= 1 && n <= 65535
then Ok(n)
else Err("Port out of range") Optional values are Option<T>, either Some(value) or None. No null.
Testing
Tests go in @test sections right next to the code.
Run them with blc test --json.
fn clamp(v: Int, lo: Int, hi: Int) -> Int =
if v < lo then lo
else if v > hi then hi
else v
@test
test "below" = clamp(-5, 0, 100) == 0
test "in range" = clamp(50, 0, 100) == 50
test "above" = clamp(200, 0, 100) == 100Web Server
Here's a full HTTP server. Switch to @prelude(server) to
get routing, JSON, and database modules.
@prelude(server)
fn list_users(req) =
Response.json([{ id: 1, name: "Alice" }])
fn get_user(req) =
let id = Request.param(req, "id")
Response.json({ id: id, name: "Alice" })
fn main!() -> {Http} Unit =
let app = Router.new()
|> Router.get("/users", list_users)
|> Router.get("/users/:id", get_user)
Server.listen!(app, 3000)