← Learn · harsh-lang.com

Harsh

Rust without the braces.  The cheat sheet.

A quick reference to everything you write daily, spelled in Harsh — as of Harsh 0.2.0. It does not teach: The Harsh Programming Language does, and reading it is not optional. Keep this page beside it. Every example on this page is transpiled by check.sh, so none of it can fall behind the language.

Basics

Task Harsh
Bind a value let answer = 42
Mutable binding let mut count = 0
Type annotation let ratio: f64 = 0.5
Several at once let (x, y) = (1, 2)
Constant const MAX_USERS: u32 = 100
Static static GREETING: &str = "hello"
Shadowing let n = n * 2
Print a line println! "Hello, {name}!"
Print with arguments println! "{} + {} = {}" a b (a + b)
Debug print println! "{:?}" v
Print to stderr eprintln! "warning: {msg}"
Comment let x = 1 // to the end of the line
Call a function let s = add 2 3
Call with no arguments let t = now$
A non-atom argument let s = add (n * 2) (-1)
A reference argument let n = count (&w)
One argument after a constructor let r = Some &x
A range argument let s = total 0..n
A call's tuple item let first = (split_pair text).0
A method let n = text <- len$
A method with arguments let parts = line <- split ","
A comparison with a negative let low = n < -1
A tuple's item let first = pair.0
A field let w = rect <- width
A path let m = std.cmp.max 3 7

The rule behind every call: arguments follow the function, one atom each; anything that is not an atom — an operator, a prefix -, & or * — goes in parentheses. f$ calls with no arguments.

Operators and precedence

Task Harsh
Arithmetic let r = (a + b) * c / d % e
Compare let ok = a >= b && c != d
Bitwise let m = (a & b) | (c ^ d) << 2
Compound assignment total += price * qty
Cast let f = n as f64
Range, exclusive and inclusive let r = 0..10 · let s = 1..=10
Negative argument let a = abs_diff (-3) 4
Reference argument let n = count_words (&text)
Dereference argument let v = double (*p)
Dereference then field let x = (*p) <- x
Error propagation let n = s <- parse.<i32>$?

Tightest first: atoms (f$, v[i], ( … )), application f a b, the chain <-, ?, the prefix operators, Rust's operators in Rust's order, the pipes, assignment. Two traps: f -1 is a subtraction, and *p <- x dereferences the field x, not p.

Strings and characters

Task Harsh
String literal (&str) let s = "hello"
Owned String let mut s = String.from "hello"
Empty String let mut s = String.new$
Append s <- push_str " world"
Append a character s <- push '!'
Format into a String let msg = format! "{name} is {age}"
Length in bytes let n = s <- len$
Length in characters let n = s <- chars$ <- count$
Slice let hello = &s[0..5]
Contains let found = s <- contains "lo"
Replace let t = s <- replace "l" "L"
Upper case let up = s <- to_uppercase$
Trim let t = line <- trim$
Split into words let words: Vec<&str> = s <- split_whitespace$ <- collect$
Parse a number let n: i32 = "42" <- parse$ <- unwrap$
Number to String let s = 42 <- to_string$
Character let c = 'h'
Character code let code = 'A' as u32
Loop over characters for c in s <- chars$: println! "{c}"
Raw string let path = r"C:\temp\new"

Numbers

Task Harsh
Integer types let a: i8 = -5 · let b: u64 = 5 · let i: usize = 0
Floating point let x: f64 = 2.5
Literal with type let n = 255u8
Separators let big = 1_000_000
Hex, binary let h = 0xff · let b = 0b1010
Largest value let m = u32.MAX
Checked arithmetic let r = a <- checked_add b
Wrapping arithmetic let r = a <- wrapping_mul b
Power let p = 2i32 <- pow 10
Square root let r = 2.0f64 <- sqrt$
Round let r = x <- round$
Absolute value let a = n <- abs$
Minimum, maximum let m = a <- min b
Convert, may fail let small = u8.try_from big
Convert, cannot fail let wide = i64.from n

Collections

Task Harsh
Vector literal let v = vec! 1 2 3
Empty vector let mut v: Vec<i32> = Vec.new$
Push, pop v <- push 4 · let last = v <- pop$
Index let first = v[0]
Index, safely let maybe = v <- get 10
Length, emptiness let n = v <- len$ · let e = v <- is_empty$
Contains let has = v <- contains (&3)
Sort v <- sort$
Sort by a key v <- sort_by_key (|p| p <- age)
Reverse v <- reverse$
Slice let middle = &v[1..3]
Array let a = [1, 2, 3]
Array of zeros let z = [0; 8]
Hash map let mut ages = HashMap.new$
Insert ages <- insert "Ana" 31
Look up let a = ages <- get "Ana"
Count with entry *counts <- entry word <- or_insert 0 += 1
Remove ages <- remove "Ana"
Hash set let mut seen = HashSet.new$
Insert into a set let new = seen <- insert 7
Double-ended queue let mut q = VecDeque.new$

Comprehensions

Task Harsh
A Vec by comprehension let squares = list~ n * n for n in 1..=10
With a condition let evens = list~ n for n in 0..20 if n % 2 == 0
Two loops let pairs = list~ (a, b) for a in 1..4 for b in a..4
A set let lengths = set~ w <- len$ for w in &words
A map, key => value let index = dict~ w => i for (i, w) in words <- iter$ <- enumerate$
A lazy generator let squares = g~ n * n for n in 1..

Control flow

Task Harsh
if, inline let sign = if n < 0: -1 else: 1
if as a statement if n < 0: println! "negative"
Several branches let size = if n < 10: "small" else if n < 100: "medium" else: "large"
for over a range for i in 0..10: println! "{i}"
for over a collection for x in &v: println! "{x}"
With the index for (i, x) in v <- iter$ <- enumerate$: println! "{i}: {x}"
while while n > 1: n /= 2
Endless loop loop: if tick$: break
loop with a value let found = loop: break 42
Labelled loop 'outer: for i in 0..10: for j in 0..10: if i * j > 20: break 'outer
Skip an iteration for i in 0..10: if i % 2 == 0: continue
if let if let Some x = maybe: println! "{x}"
while let while let Some top = stack <- pop$: println! "{top}"
let … else let Some n = first else: return
Discard a block's last value for w in words: seen <- insert w then a line () beneath
A block as an argument let r = f ({ let u = 3; u * u })

Blocks over several lines are what you will write most:

fn classify (n: i32) -> &'static str:
    if n < 0:
        "negative"
    else if n == 0:
        "zero"
    else:
        "positive"

fn count_new (words: &[&str]) -> usize:
    let mut seen = std.collections.HashSet.new$
    for w in words:
        seen <- insert w
        ()
    seen <- len$

Pattern matching

Task Harsh
Two arms, inline let name = match n\ 0 => "zero", _ => "other"
A pattern with a value let n = match maybe\ Some x => x, None => 0
Destructure a tuple let (a, b) = pair
Destructure a struct let Point\ x, y = p
Part of a struct let Point\ x, .. = p
Rename while destructuring let Point\ x: px, y: py = p
Destructure in a closure's parameter let xs: Vec<f64> = ps <- iter$ <- map (|(Point\ x, ..)| *x) <- collect$
matches! let digit = matches! c ('0'..='9')

More than two arms go one per line, no commas:

fn describe (n: i32) -> String:
    match n\
        0 => String.from "zero"
        1 | 2 | 3 => String.from "a few"
        4..=9 => String.from "several"
        x if x < 0 => format! "negative {x}"
        _ => String.from "many"

Functions and closures

Task Harsh
A function fn add (a: i32) (b: i32) -> i32: a + b
No parameters fn answer$ -> i32: 42
One bare parameter fn double n: i32 -> i32: n * 2
No return value fn greet (name: &str): println! "Hello, {name}"
Early return fn first_even (v: &[i32]) -> Option<i32>: v <- iter$ <- find (|x| *x % 2 == 0) <- copied$
A closure let inc = |x| x + 1
Annotated closure let add = |a: i32, b: i32| -> i32: a + b
Capturing by value let show = move || println! "{name}"
Call a closure let three = inc 2
Pipe forward let r = 5 |> double |> inc
Pipe backward let r = inc <| double <| 5
Several values into one call let s = 2 3 |> add
Partial application let add10 = 10 |> add

A body of several statements goes beneath its header; so may any body, for room:

fn double n: i32 -> i32:
    n * 2

fn apply<F: Fn (i32) -> i32> (f: F) (x: i32) -> i32:
    f x

fn adder n: i32 -> impl Fn (i32) -> i32:
    move |x| x + n

fn print_all<T: std.fmt.Debug> (items: &[T]):
    for i in items:
        println! "{:?}" i

Structs and enums

Task Harsh
Record struct, inline struct Size\ w: f64, h: f64
Tuple struct struct Meters f64
Unit struct struct Marker
Build a struct let p = Point\ x = 1.0, y = 2.0
Field shorthand let p = Point\ x, y
Struct update let q = Point\ x = 5.0, ..p
A struct as an argument cards <- push (Card\ id = 1), Ok (Card\ id = 1) — isolated; bare, it is refused. Over several lines: the fields one unit past Card, the closing paren under the opening one
Tuple struct value let d = Meters 3.5
Read a tuple field let m = d.0
Enum, inline enum Dir\ North, South, East, West
Enum value let d = Dir.North
Derive traits #[derive Debug Clone PartialEq] above the type

Declarations take no mark; their fields or variants go beneath, and a literal with more than two fields goes one field per line:

#[derive Debug Clone]
struct Point
    x: f64
    y: f64

enum Shape
    Circle f64
    Rect
        w: f64
        h: f64
    Empty

fn origin_config$ -> Config:
    Config\
        name = String.from "origin"
        verbose = true
        retries = 3

struct Config
    name: String
    verbose: bool
    retries: u32

Methods and traits

use std.fmt

struct Circle
    r: f64

impl Circle
    fn new r: f64 -> Self:
        Self\ r
    fn area (&self) -> f64:
        3.14159 * self <- r * self <- r
    fn grow (&mut self) (by: f64):
        self <- r += by

trait Describe
    fn describe (&self) -> String
    fn shout (&self) -> String:
        self <- describe$ <- to_uppercase$

impl Describe for Circle
    fn describe (&self) -> String:
        format! "a circle of radius {}" (self <- r)

impl fmt.Display for Circle
    fn fmt (&self) (f: &mut fmt.Formatter) -> fmt.Result:
        write! f "Circle({})" (self <- r)
Task Harsh
Call an associated function let c = Circle.new 2.0
Call a method let a = c <- area$
Call a mutating method c <- grow 1.0
Trait object let items: Vec<Box<dyn Describe>> = vec! (Box.new c)
impl Trait argument fn show (item: &impl Describe): println! "{}" (item <- describe$)
Lifetime fn longest<'a> (x: &'a str) (y: &'a str) -> &'a str: if x <- len$ > y <- len$: x else: y

Ownership and borrowing

Task Harsh
Move let b = a
Clone let b = a <- clone$
Borrow let r = &s
Borrow mutably let m = &mut s
Pass a borrow let n = length (&s)
Pass a mutable borrow change (&mut s)
Drop early drop guard
Box let b = Box.new 5
Shared ownership let a = Rc.new 5 · let b = Rc.clone (&a)
Interior mutability let c = RefCell.new 0 · *c <- borrow_mut$ += 1
Thread-safe sharing let n = Arc.new (Mutex.new 0)

Errors

Task Harsh
Option let found: Option<i32> = Some 3
Default when absent let n = found <- unwrap_or 0
Result let r: Result<i32, String> = Ok 1
Propagate with ? let n = text <- parse.<i32>$?
Convert Option to Result let v = found <- ok_or "missing"?
Transform the value let doubled = found <- map (|x| x * 2)
Panic panic! "unreachable state: {s}"
Unwrap with a message let f = File.open path <- expect "cannot open"
main that returns errors fn main$ -> Result<(), Box<dyn Error>>: run$

A custom error type:

use std.fmt

#[derive Debug]
enum AppError
    NotFound String
    Invalid u32

impl fmt.Display for AppError
    fn fmt (&self) (f: &mut fmt.Formatter) -> fmt.Result:
        match self\
            AppError.NotFound name => write! f "{name} not found"
            AppError.Invalid code => write! f "invalid code {code}"

impl std.error.Error for AppError {}

Iterators

Task Harsh
Map let doubled: Vec<i32> = v <- iter$ <- map (|x| x * 2) <- collect$
Filter let big: Vec<&i32> = v <- iter$ <- filter (|x| **x > 10) <- collect$
Sum let total: i32 = v <- iter$ <- sum$
Count let n = v <- iter$ <- filter (|x| **x > 0) <- count$
Any, all let any_neg = v <- iter$ <- any (|x| *x < 0)
Find let first = v <- iter$ <- find (|x| **x > 3)
Fold let product = v <- iter$ <- fold 1 (|acc, x| acc * x)
Zip let pairs: Vec<_> = a <- iter$ <- zip (&b) <- collect$
Take, skip let firsts: Vec<_> = v <- iter$ <- take 3 <- collect$
Max let top = v <- iter$ <- max$
Into a String let joined = words <- join ", "

A chain of three steps or more goes vertical:

fn evens_squared (v: &[i32]) -> Vec<i32>:
    v
        <- iter$
        <- filter (|x| *x % 2 == 0)
        <- map (|x| x * x)
        <- collect$

Modules and imports

Task Harsh
Import an item use std.collections.HashMap
Import several use std.io.(self, Write)
Import with a new name use std.fmt.Result as FmtResult
Import everything use std.collections.*
Public item pub fn area (w: f64) (h: f64) -> f64: w * h
From the crate root use crate.shapes.Circle
From the parent module use super.helper
A module in its own file mod shapes

Testing

fn add (a: i32) (b: i32) -> i32:
    a + b

#[cfg test]
mod tests
    use super.*

    #[test]
    fn adds_two_numbers$:
        assert_eq! (add 2 3) 5

    #[test]
    #[should_panic]
    fn index_out_of_range$:
        let v = vec! 1 2 3
        v[99]
        ()
Task Harsh
Assert assert! (x > 0)
Assert equal assert_eq! (add 2 2) 4
Assert with a message assert! ok "expected success, got {code}"
Run the tests hrs test (a command, not Harsh)

Concurrency and async

Task Harsh
Spawn a thread let h = thread.spawn (|| work$)
Wait for it let result = h <- join$ <- unwrap$
A channel let (tx, rx) = mpsc.channel$
Send, receive tx <- send 42 <- unwrap$ · let n = rx <- recv$ <- unwrap$
Shared counter *counter <- lock$ <- unwrap$ += 1
Async function async fn fetch (url: &str) -> String: download url <- await
Await let page = fetch "https://example.com" <- await

A thread whose body spans lines, inside a loop; the () after the body says the loop body is worth nothing:

fn spawn_all$:
    let (tx, rx) = std.sync.mpsc.channel$
    for id in 0..3:
        let tx = tx <- clone$
        std.thread.spawn move ||:
            tx <- send (id * id) <- unwrap$
        ()
    drop tx
    for n in rx: println! "{n}"

Linear algebra

Add hrs_std to the project once: hrs add hrs_std.

Task Harsh
Matrix let a = m~ [1.0 2.0; 3.0 4.0]
Vector let b = v~ [5.0, 6.0]
Product let c = &a * &a
Element let e = a[(0, 1)]
Determinant let d = a <- det$
Inverse, or a panic let i = a <- inv$
Solve a x = b, or a panic let x = a <- solve (&b)
Solve, the caller decides let x = a <- try_solve (&b)?
Norm let n = b <- norm$

solve and inv stop the program on a singular matrix, as Julia does; try_solve and try_inv return Err (LinAlgError.Singular) instead.

Macros

Task Harsh
Call a Rust macro let v = vec! 1 2 3
A Rust macro with a block assert! (v <- is_empty$ == false)
Call a Harsh macro let squares = list~ n * n for n in 1..5
A macro by its path let s = hrs_std.list~ n for n in 0..3

Harsh's own macros, written ~, are expanded by hrs and are gone from the Rust. A macro's own language — an rsx! tree, view! markup, select! arms — goes in braces as Rust, with Harsh in holes @: … :@. The Book's chapter 23 teaches writing both kinds.

The hrs tool

Command What it does
hrs new hello a project laid out for Harsh
hrs build transpile, then cargo build; errors point at .hrs lines
hrs run build and run
hrs test build and run the tests
hrs check · hrs lint cargo check, cargo clippy, mapped to Harsh
hrs watch rebuild on every save
hrs fmt · hrs fmt --check lay out the files by Harsh's rules
hrs test at a workspace's root each member in turn: a Harsh member by hrs, a Rust one by cargo (also build, check, lint, fmt)
hrs -c doc cargo in export/, the Rust project every build writes
hrs -c publish the Rust in export/, to crates.io
hrs add hrs_std add Harsh's crates, to Hrs.toml (Rust crates: cargo add, to Cargo.toml)
hrs migrate bring an older project up to date: Hrs.toml (0.3.0), export/ (0.9.0)
hrs in.hrs -o out.rs transpile one file, anywhere; the .rs starts with a header naming its source
hrs a.hrs b.hrs several files, each beside its .hrs, seeing each other's functions
hrs --check a.hrs b.hrs write nothing; fail if a .rs is not what its .hrs makes (for CI)
hrs-from file.rs convert Rust to Harsh
Editors hrs-lsp: Enter and Tab by layout, format on save, hover, definitions, completion
Jupyter the harsh-kernel: a cell's last value is shown; a last line () hides it

Three ways to use Harsh

Published to Harsh's registry Published to crates.io
Harsh project (hrs new) #1: a Harsh crate #2: a Rust crate written in Harsh
Rust project (cargo new) (empty) #3: Harsh files in a Rust crate
  • #1: hrs new mylib, hrs build, hrs test; used by path today, in the user's Hrs.toml: mylib = { path = "../mylib" }, Harsh macros included. Harsh's registry is still to come.
  • #2: hrs new, hrs build, then hrs -c publish: the Rust in export/. A Harsh crate it uses is named by its own export/ and its version: hrs -c publish that one first. Harsh macros unfold before the Rust is written, so the published crate has none.
  • #3: cargo new; each .hrs file beside the .rs it produces, with hrs src/parser.hrs src/lexer.hrs (files given together see each other's functions); commit the .rs, put hrs --check src/*.hrs in CI, then cargo publish.

Layout style

hrs fmt applies the countable rules; the rest is judgment:

  • A chain of three steps or more goes vertical, every <- under the first.
  • A literal of three fields or more goes one field per line, no commas.
  • An if with three branches or more goes on several lines.
  • A literal inside a literal on one line is parenthesised: Dog\ inner = (Animal\ name = n), good = true.
  • An inline if c: a else: b is left as written; too long, write a block if with each value beneath its branch.
  • A match with more than two arms goes one arm per line.
  • A call made long by a long argument goes vertical, one argument per line.
  • No semicolons: a block that discards its last value ends with a line ().

Naming

As in Rust: snake_case for functions, variables and modules; CamelCase for types and traits; SCREAMING_SNAKE_CASE for constants and statics. Harsh files end in .hrs.

Where to read more

  • The Harsh Programming Language — the Book; read it all.
  • Harsh by Example — one runnable page per feature.
  • Harsh Design Patterns and The Harshonomicon — the companions.
  • The language guide — every rule, with its Rust beside it.