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 * 2

When 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 * h

Error 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) == 100

Web 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)

Next Steps

  • Learn: types, effects, syntax, and operators
  • API: every module and function
  • Source: contribute on GitHub