← Learn · harsh-lang.com

Harsh

Harsh by Example

Short programs, one idea each, every one of them compiled and run to produce the output you see. If you know how to program, you can read this book from the top and write Harsh by the end of it.

It is the lookup book: when you want to know how something is written, find the page and copy the shape. The Harsh Programming Language is the one that teaches the ideas — ownership, borrowing, traits, lifetimes — and is the place to go when an example here works but you cannot see why. The Harsh Language Guide is the third: it sets every Harsh spelling beside the Rust it stands for, for readers who already know Rust.

Rust without braces. Rust with pipes, partial application, comprehensions and linear algebra. Rust for functional programming, data science and machine learning.

The last three lines are pages 7, 16 and 17: the pipes and partial application, comprehensions, and matrices with linear algebra — what Harsh adds to Rust, and the reason to use it.

There is no Rust in this book. Every program here is transpiled, compiled and run before it reaches the page, so what you read is what the machine did.

The topics, and many of the example programs, follow Rust by Example, rewritten in Harsh. ATTRIBUTION.md has the licence and the details.

1. Hello and printing

1.1 Hello, world!

The whole program:

fn main$:
    println! "Hello, world!"
Hello, world!

fn main$ is the function every program starts from. The $ says it takes no arguments — Harsh writes apply to nothing that way, and you will see it again on every call that takes none, like String.new$.

println! is a macro, which the ! tells you, and its arguments are simply written after it with spaces between them. No parentheses, no commas: that is how every call in Harsh is written, macro or function.

1.2 Comments

fn main$:
    // A line comment runs from the slashes to the end of the line.
    let x = 5

    /* A block comment can sit inside an expression, */
    let y = x + /* like this */ 5

    println! "x = {x}, y = {y}"
x = 5, y = 10

Two kinds, both Rust's: // to the end of the line, and /* … */ anywhere, including in the middle of an expression. Block comments nest.

1.3 Printing

The string comes first, then the values that fill its holes:

fn main$:
    // Each `{}` is filled by the next argument, juxtaposed after the string.
    println! "{} days" 31

    // A name in the braces takes it from a variable of that name.
    let actor = "the sun"
    println! "{actor} rises"

    // `{n}` picks the nth argument, counting from zero.
    println! "{0} is {1}, {1} is {0}" "this" "that"

    // A width after `:` pads the value; `>` right-aligns it.
    println! "[{:>5}]" 42

    // And a `{{` is a literal brace.
    println! "{{ not a hole }}"
31 days
the sun rises
this is that, that is this
[   42]
{ not a hole }

The hole is {}. What goes between the braces is a small language of its own — a name, a number, a width, an alignment — and all of it is Rust's, unchanged. The thing to notice is what is outside the braces: the arguments are juxtaposed, so println! "{} {}" a b needs no commas.

An argument that is not a single atom is isolated in parentheses, exactly as it would be for a function:

println! "{}" (a + 1)
println! "{}" (p <- name)

That second one is a field access. <- is how Harsh reads into a value, and it is an operator, so it needs the parentheses when used as an argument.

1.4 Debug

Most types cannot be printed until you say how. The quickest way is to derive the programmer's spelling:

// `Debug` is derived: it prints a value the way a programmer reads it.
#[derive Debug]
struct Point\ x: i32, y: i32

fn main$:
    let p = Point\ x = 3, y = 4

    // `{:?}` asks for the Debug spelling...
    println! "{:?}" p

    // ...and `{:#?}` for the same thing laid out over several lines.
    println! "{:#?}" p
Point { x: 3, y: 4 }
Point {
    x: 3,
    y: 4,
}

#[derive Debug] is an attribute, and its arguments juxtapose like everything else. {:?} asks for that spelling and {:#?} for the same thing over several lines.

Note the struct declaration: struct Point\ x: i32, y: i32. The \ opens a specification block — Rust's { a, b } groupings — and it always follows the thing it specifies. The same \ builds the value: Point\ x = 3, y = 4. Written over several lines, the fields simply go beneath:

struct Point
    x: i32
    y: i32

1.5 Display

Display is the spelling meant for whoever reads the output, and you write it yourself:

use std.fmt

struct Point\ x: i32, y: i32

// `Display` is written by hand: it is the spelling meant for a reader.
impl fmt.Display for Point
    fn fmt (&self) (f: &mut fmt.Formatter) -> fmt.Result:
        write! f "({}, {})" (self <- x) (self <- y)

fn main$:
    let p = Point\ x = 3, y = 4
    println! "{}" p
(3, 4)

impl fmt.Display for Point takes no mark at all — an item body follows its header directly, with the items beneath it. . is the path separator, so fmt.Display is Rust's fmt::Display. Inside, write! takes the formatter and the same string-and-holes as println!.

Once a type has Display, {} works on it, and to_string$ comes free.

2. Primitives

2.1 Literals and operators

fn main$:
    // A suffix names the type; an underscore is a spacer.
    let big = 1_000_000u64
    let small: i8 = -7
    let ratio = 2.5f32

    // Integer, float, bool, char.
    let yes = true
    let letter = 'H'

    println! "{big} {small} {ratio} {yes} {letter}"

    // The operators are Rust's, unchanged.
    println! "{}" (1 + 2 * 3)
    println! "{}" (7 / 2)          // integer division truncates
    println! "{}" (7 % 2)
    println! "{}" (1u32 << 5)
    println! "{}" (true && false)
1000000 -7 2.5 true H
7
3
1
32
false

Numbers, bool and char are Rust's, and so is every operator in that list. Harsh changes none of it. The one thing to watch is the parentheses: 1 + 2 * 3 is an operator expression, and an operator expression handed to a call is isolated, so it is println! "{}" (1 + 2 * 3).

2.2 Tuples

// A tuple struct: its fields juxtapose, like any application.
// (Parens would hold *one* payload: `struct Wrapped (i32, i32)` has a
// single field that is a pair.)
#[derive Debug]
struct Matrix f32 f32 f32 f32

fn transpose (m: Matrix) -> Matrix:
    // Fields of a tuple are reached by number, with a dot.
    Matrix m.0 m.2 m.1 m.3

fn main$:
    // A tuple value: one construct, commas inside.
    let pair = (1, "one")
    println! "{:?}" pair

    // Taking it apart binds both halves.
    let (n, name) = pair
    println! "{n} is {name}"

    // Constructing juxtaposes, as every application does.
    let m = Matrix 1.1 1.2 2.1 2.2
    println! "{:?}" m
    println! "{:?}" (transpose m)
(1, "one")
1 is one
Matrix(1.1, 1.2, 2.1, 2.2)
Matrix(1.1, 2.1, 1.2, 2.2)

Two different things share the parentheses here, and it is worth keeping them apart.

A tuple value is (1, "one") — one construct, commas inside, exactly as in Rust. A tuple struct declares its fields by juxtaposition: struct Matrix f32 f32 f32 f32. Parentheses in a declaration would hold a single payload, so struct Wrapped (i32, i32) is one field that happens to be a pair, not two fields.

Constructing follows the same rule as any call — Matrix 1.1 1.2 2.1 2.2 — and reaching a field by number is m.0, with a dot, as for any tuple.

2.3 Arrays and slices

fn main$:
    // An array: fixed length, all one type.
    let xs: [i32; 5] = [1, 2, 3, 4, 5]

    // `[value; count]` fills it.
    let zeros = [0; 3]

    // Indexing is tight against the name.
    println! "{} {}" xs[0] xs[4]
    println! "{:?} {:?}" xs zeros
    println! "{}" (xs <- len$)

    // A slice borrows part of it.
    let middle = &xs[1..4]
    println! "{:?}" middle

    // Out of range is a panic, not a wrong answer -- see the next example.
    for x in xs <- iter$:
        print! "{x} "
    println! ""
1 5
[1, 2, 3, 4, 5] [0, 0, 0]
5
[2, 3, 4]
1 2 3 4 5

An index is written tight against the name, xs[0]. That tightness is what makes it part of the atom: in an application, f xs[0] passes the element, while a spaced f xs [0] is refused rather than guessed at.

A slice borrows a run of it: &xs[1..4].

Reading past the end is a panic, which is to say it stops rather than returning something wrong:

fn main$:
    let xs = [1, 2, 3]
    let n = 5
    println! "reaching for element {n}"
    println! "{}" xs[n]
reaching for element 5
thread 'main' (22636) panicked at out_of_range.hrs:6:20:

3. Custom types

3.1 Structures

#[derive Debug]
struct Person
    name: String
    age: u8

// A unit struct: no fields at all.
struct Unit

// A tuple struct: its fields juxtapose.
struct Pair i32 f32

fn main$:
    let peter = Person\ name = String.from "Peter", age = 27
    println! "{:?}" peter

    // The same thing over several lines: the fields go beneath.
    let jane =
        Person\
            name = String.from "Jane"
            age = 31
    println! "{} is {}" (jane <- name) (jane <- age)

    let pair = Pair 1 0.1
    println! "{} {}" pair.0 pair.1

    // A struct is taken apart by naming its fields, the same `\`.
    let Pair x y = pair
    println! "{x} {y}"

    let _u = Unit
Person { name: "Peter", age: 27 }
Jane is 31
1 0.1
1 0.1

Three shapes, and the mark is the same in all of them: none. A declaration's body follows its name directly, with the fields beneath it, or inline after a \.

struct Person              struct Person\ name: String, age: u8
    name: String
    age: u8

Building a value uses the same \, with = for each field: Person\ name = …, age = 27. Over several lines the fields go beneath, and the opener starts its own line after the =. Taking one apart is the mirror image — let Pair x y = pair — which is the general rule that matching is constructing, read backwards.

Handed to a function, a literal is isolated like any argument of several tokens, a constructor's included: people <- push (Person\ name = n, age = 27), Some (Person\ …); written bare, it is refused. Over several lines, its fields stand one unit past its name and its ) under its (:

people <- push (Person\
                    name = String.from "Ada"
                    age = 36
               )

3.2 Enums

// A variant can carry nothing, a payload, or named fields.
#[derive Debug]
enum Event
    PageLoad
    Click i64 i64
    KeyPress char
    Paste
        text: String
        at: usize

fn describe (e: Event) -> String:
    match e\
        Event.PageLoad => String.from "the page loaded"
        Event.Click x y => format! "clicked at {x}, {y}"
        Event.KeyPress c => format! "pressed '{c}'"
        Event.Paste\ text, at => format! "pasted {:?} at {}" text at

fn main$:
    let events =
        [
            Event.PageLoad,
            Event.Click 20 80,
            Event.KeyPress 'q',
            Event.Paste\ text = String.from "hi", at = 3,
        ]
    for e in events:
        println! "{}" (describe e)
the page loaded
clicked at 20, 80
pressed 'q'
pasted "hi" at 3

A variant carries nothing, a payload (Click i64 i64, juxtaposed like a tuple struct), or named fields (Paste with its fields beneath). Constructing and matching use the shape the variant was declared with, and the record variant takes the \ it was declared with:

Event.Paste\ text = String.from "hi", at = 3        building
Event.Paste\ text, at => …                          matching

match e\ opens the arms with \, because a match's arms are a specification block like a struct's fields — Rust writes both with braces and commas. The arms then go beneath, or inline after the \:

match n\ 0 => "zero", _ => "more"

3.3 Constants

// A `const` is inlined wherever it is used; a `static` has one address.
const THRESHOLD: i32 = 10
static LANGUAGE: &str = "Harsh"

fn is_big (n: i32) -> bool:
    n > THRESHOLD

fn main$:
    println! "{LANGUAGE}: threshold is {THRESHOLD}"
    println! "{} {}" (is_big 5) (is_big 50)
Harsh: threshold is 10
false true

const is inlined at each use; static names one place in memory. Both need their type, and both are written at the top level or inside a function.

4. Bindings and types

4.1 Bindings

fn main$:
    // A binding is immutable unless it says otherwise.
    let x = 1
    let mut y = 1
    y += 1
    println! "{x} {y}"

    // Shadowing: a second `let` of the same name, even of another type.
    let spaces = "   "
    let spaces = spaces <- len$
    println! "{spaces}"

    // A block has its own scope, and is an expression.
    let outer = 1
    do:
        let outer = 2
        println! "inner sees {outer}"
    println! "outer is still {outer}"

    // Declared first, given a value later.
    let answer
    if y > 1:
        answer = "big"
    else:
        answer = "small"
    println! "{answer}"
1 2
3
inner sees 2
outer is still 1
big

A let binds a name, and the binding is immutable unless it says mut. A second let of the same name shadows the first, and may give it another type — which is how spaces goes from a string to its length without a second name.

do: opens a block of statements. It is the mark for "this is a block", and you will meet it wherever a block is needed but no keyword has already said so: as a scope of its own, as a closure's body, as a value.

Assigning to a binding that never said mut is refused before the program runs:

fn main$:
    let x = 1
    x = 2
    println! "{x}"
error[E0384]: cannot assign twice to immutable variable `x`
  --> immutable.hrs:3:5
   |
 2 |     let x = 1
   |         - first assignment to `x`
 3 |     x = 2
   |     ^^^^^ cannot assign twice to immutable variable
   = help: consider making this binding mutable (hrs 2:9)

4.2 Types

fn main$:
    // An annotation names the type; otherwise it is inferred.
    let n: u8 = 200
    let guessed = 200        // i32 by default

    // A cast is explicit, with `as`.
    let wide = n as u32 + 1
    let narrowed = 300i32 as u8      // wraps: 300 - 256
    println! "{n} {guessed} {wide} {narrowed}"

    // A float to an integer truncates towards zero.
    println! "{}" (2.9f64 as i32)

    // An alias is another name for a type, not a new type.
    let total: Meters = 5
    println! "{total}"

// Aliases are items, so they live outside the function too.
type Meters = u32
200 200 201 44
2
5

Every value has a type, written after : when it is not obvious. as casts, truncating or wrapping rather than guessing. type makes an alias — another name for the same type, not a new one.

Note where the type item sits in that program: after the function that uses it. Items are not statements, and their order does not matter.

5. Flow control

5.1 if and else

fn main$:
    let n = 7

    // `if` is an expression, so it has a value.
    let size =
        if n < 5:
            "small"
        else if n < 10:
            "medium"
        else:
            "large"
    println! "{size}"

    // On one line, the body follows the colon.
    let parity = if n % 2 == 0: "even" else: "odd"
    println! "{parity}"
medium
odd

if is an expression, so it can be the value of a let. Written over several lines, the opener ends its line and the body goes beneath; written on one line, the body follows the colon.

Each else answers the nearest open if.

5.2 Loops

fn main$:
    // `loop` runs until something breaks out of it -- and `break` may
    // carry a value, which makes the loop an expression.
    let mut n = 0
    let stopped_at =
        loop:
            n += 1
            if n == 5:
                break n * 10
    println! "stopped at {stopped_at}"

    // `while` tests first.
    let mut count = 3
    while count > 0:
        print! "{count} "
        count -= 1
    println! "go"

    // `for` walks anything iterable; `..` excludes the end, `..=` includes it.
    for i in 1..4:
        print! "{i} "
    for i in 1..=3:
        print! "{i} "
    println! ""

    // A label lets `break` leave the outer loop.
    'rows: for r in 0..3:
        for c in 0..3:
            if r * c == 4:
                break 'rows
            print! "{r}{c} "
    println! ""
stopped at 50
3 2 1 go
1 2 3 1 2 3 
00 01 02 10 11 12 20 21

loop, while and for, all with their bodies beneath a :. Two things are worth pointing out.

break may carry a value, which makes loop an expression — that is where stopped_at comes from. And a loop may be labelled, 'rows:, so that break can leave an outer loop rather than the one it stands in.

5.3 match

fn describe (n: i32) -> String:
    match n\
        0 => String.from "zero"
        1 | 2 => String.from "a couple"
        3..=9 => String.from "a few"
        _ if n < 0 => String.from "negative"
        other => format! "{other}, which is a lot"

fn main$:
    for n in [0, 2, 5, -1, 40]:
        println! "{}" (describe n)

    // Destructuring in a match arm.
    let point = (3, 0)
    let where_ =
        match point\
            (0, 0) => "the origin"
            (_, 0) => "the x axis"
            (0, _) => "the y axis"
            _ => "somewhere else"
    println! "{where_}"
zero
a couple
a few
negative
40, which is a lot
the x axis

match x\ opens the arms with \, because they are a specification block: Rust writes them with braces and commas, and \ is how Harsh spells that. The arms go beneath, or inline after the \.

An arm's pattern is read exactly as an application is — Some n, Event.Click x y — which is the rule that matching is constructing, used backwards. A guard is an if after the pattern, and _ catches the rest.

5.4 if let, let else, while let

fn main$:
    let some: Option<i32> = Some 7
    let none: Option<i32> = None

    // `if let` runs the block only when the pattern fits.
    if let Some n = some:
        println! "got {n}"

    // With an `else` for when it does not.
    if let Some n = none:
        println! "got {n}"
    else:
        println! "got nothing"

    // `let ... else` binds or leaves: the `else` block must not fall through.
    let Some value = some else:
        println! "nothing to do"
        return
    println! "value is {value}"

    // `while let` keeps going as long as the pattern fits.
    let mut stack = vec! 1 2 3
    while let Some top = stack <- pop$:
        print! "{top} "
    println! ""
got 7
got nothing
value is 7
3 2 1

Three shorthands for the same idea: run this only if the pattern fits.

let … else: is the one to reach for when the rest of the function makes no sense without the value — the else block must leave, so after it the binding is simply there.

6. Functions

6.1 Declaring and calling

// One group per parameter. A single parameter may drop its parentheses.
fn double n: i32 -> i32:
    n * 2

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

// No parameters at all: `$`.
fn greeting$ -> &'static str:
    "hello"

// Nothing returned: no arrow.
fn announce (what: &str):
    println! "-- {what} --"

fn main$:
    announce (greeting$)
    println! "{}" (double 21)
    println! "{}" (add 1 2)

    // The last expression is the value; `return` is for leaving early.
    // `-3` is isolated: without the parens, `abs - 3` is a subtraction.
    println! "{}" (abs (-3))

fn abs n: i32 -> i32:
    if n < 0:
        return -n
    n
-- hello --
42
3
3

One group per parameter: fn add (a: i32) (b: i32). A single parameter may drop its parentheses, fn double n: i32. A comma list is refused, because it would be a second spelling of the same thing.

fn greeting$ takes nothing — the same $ as every call that applies a name to nothing.

Calling juxtaposes: add 1 2. An argument that is not one atom is isolated: (greeting$), (abs (-3)). That last one is the case to remember — without its parentheses, abs -3 reads as a subtraction, because that is what it looks like.

6.2 Methods

struct Rect
    w: f64
    h: f64

impl Rect
    // An associated function: no `self`, called through the type.
    fn square s: f64 -> Rect:
        Rect\ w = s, h = s

    // A method: `self` is the first parameter, and alone it may be bare.
    fn area (&self) -> f64:
        (self <- w) * (self <- h)

    // Taking `&mut self` to change the value.
    fn grow (&mut self) (by: f64):
        self <- w += by
        self <- h += by

fn main$:
    let mut r = Rect.square 2.0
    println! "{}" (r <- area$)
    r <- grow 1.0
    println! "{}" (r <- area$)
4
9

An impl block takes no mark at all: the items follow the header. self is a parameter like any other — alone it may be bare, fn area (&self), and beside others it is a group of its own.

<- reaches into a value, so a field is self <- w and a method call on a value is r <- area$. An associated function is reached through the type, Rect.square 2.0, because . is the path separator.

6.3 Closures

fn apply (f: impl Fn i32 -> i32) (to: i32) -> i32:
    f to

fn main$:
    // A closure: parameters between bars, then the body.
    let twice = |x: i32| x * 2
    println! "{}" (apply twice 5)

    // A block body: `|x|:` with the body beneath, or `|x| do:` inline.
    let describe = |n: i32|:
        if n > 10:
            "big"
        else:
            "small"
    println! "{}" (describe 40)

    // A closure captures what it uses.
    let base = 100
    let offset = |x: i32| x + base
    println! "{}" (offset 5)

    // In a chain, a closure with a block body is isolated in parentheses:
    // a bare block would swallow the rest of the chain.
    let v = vec! 1 2 3 4
    let evens: Vec<i32> =
        v <- iter$
          <- filter (|n|: *n % 2 == 0)
          <- cloned$
          <- collect$
    println! "{:?}" evens

    // When the chain is a statement rather than a value being bound, the
    // last closure may keep its block, with the body beneath.
    v <- iter$ <- for_each |n|:
        print! "{n} "
    println! ""
10
big
105
[2, 4]
1 2 3 4

|x| body on one line; |x|: with the body beneath; |x| do: for the same thing inline. As a trailing argument a closure may own its block, and the chain closes after it — but only when the chain is a statement. When the chain is a value being bound, isolate the closure in parentheses, as filter does here, so that the block cannot swallow what follows.

7. What Harsh adds

Everything so far has been Rust with the braces taken out. This page is the part that is Harsh's own.

7.1 The pipes

fn sub (a: i32) (b: i32) (c: i32) -> i32:
    a - b - c

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

fn main$:
    // `|>` fills a function's parameters from the left.
    println! "{}" (10 5 1 |> sub)

    // Fewer arguments than it takes: what comes back is a closure waiting
    // for the rest. This is partial application, and it is flat -- no
    // nesting, no `move |x| move |y|`.
    let from_ten = 10 |> sub
    println! "{}" (from_ten 5 1)

    // `<|` fills from the right instead.
    let take_one = sub <| 1
    println! "{}" (take_one 10 5)

    // Both at once: `1 |> sub <| 3` puts 1 first and 3 last.
    let middle = 10 |> sub <| 1
    println! "{}" (middle 5)

    // Chained, a pipe reads left to right: the value, then what happens next.
    println! "{}" (3 |> double |> double)
4
4
4
4
12

|> fills a function's parameters from the left, <| from the right. When the count reaches what the function takes, it is simply a call. When it falls short, what comes back is a closure waiting for the rest — partial application, and a flat one: a single closure over the remaining parameters, not a nest of them.

The arity comes from the declaration, which hrs has already read, so nothing has to be spelled out at the call.

Left of |> is a run of atoms, all of them arguments. To pipe the result of an application, isolate it: (f a) |> g.

7.2 Partial application

fn label (prefix: &str) (name: &str) (suffix: &str) -> String:
    format! "{prefix}{name}{suffix}"

fn main$:
    // `|>` fills the parameters from the left.
    println! "{}" ("Dr. " "Ada" "," |> label)
    // `<|` fills them from the right: this waits for a prefix.
    let with_phd = label <| "Ada" " PhD"
    println! "{}" (with_phd "Prof. ")
    // Both together leave a hole in the middle: a function of the name.
    let formal = "Dr. " |> label <| "."
    println! "{}" (formal "Ada")
    println! "{}" (formal "Grace")
Dr. Ada,
Prof. Ada PhD
Dr. Ada.
Dr. Grace.

Give a function fewer values than it takes and the result is a function waiting for the rest. |> fills parameters from the left, <| from the right, and both together leave a hole in the middle: "Dr. " |> label <| "." is a function of the name alone. There is no placeholder; the hole is what is left. Chapter 14 of the Book has the whole story.

7.3 Macros

Harsh's macros are organised by who wrote them: in Harsh or in Rust -- the mark says so, name~ or name! -- and yours or imported from a library. The Book's sections 23.5 to 23.8 take each part in full, and follow the Rust Book's own chapter on macros.

Harsh macros (~)

Your own, written in Harsh.

Declarative macros
// A Harsh macro is matched and expanded by `hrs`, in Harsh, before anything
// is transpiled. The mark is `~`.
macro_rules~ greet
    (($who:expr)) => do:
        println! "hello, {}" $who

// A repetition takes `*`, `+` or `?`, exactly as Rust's does.
macro_rules~ list_of
    ($( ($x:expr) )*) => do: [$( $x ),*]

fn main$:
    greet~ "world"

    // The arguments are a token stream: everything to the end of the line,
    // and every line indented beneath it.
    let small = list_of~ 1 2 3
    let bigger =
        list_of~ 1 2 3
                     4 5 6
    println! "{:?} {:?}" small bigger

    // To chain on the result, isolate the call.
    println! "{}" ((list_of~ 1 2 3) <- len$)
hello, world
[1, 2, 3] [1, 2, 3, 4, 5, 6]
3

macro_rules~ defines a macro that hrs expands itself, in Harsh, before anything is transpiled — so the generated Rust holds no macro at all, and what a macro produces is Harsh you could have written by hand.

A call passes a token stream, not arguments: everything after name~ to the end of the line, and every line indented deeper beneath it. Nothing in that stream is interpreted — a comma is the DSL's, a \ is the DSL's — which is why a macro can invent a grammar of its own. The stream ends at the close of a group the call sits inside, so (list_of~ 1 2 3) <- len$ chains on the result.

Procedural macros

Programs, written in Harsh in a crate of their own, mirroring Rust's three forms; each needs two crates to show, so the Book teaches them (23.5).

Custom derive Macros

A pub fn marked #[proc_macro_derive~ Describe], called #[derive~ Describe] above a struct, an enum or a union: it receives the item and adds code after it.

Attribute-Like Macros

#[proc_macro_attribute~] on a pub fn taking two streams, the attribute's arguments and the item, called #[route~ GET "/"] or #[route~], and replacing the item (the Book's 23.5 has the Rust Book's route example).

Function-Like Macros

A pub fn marked #[proc_macro~], called like a declarative macro, hello_macro~ world: it receives world and returns the Harsh that takes the call's place.

Rust macros (!)

Your own, written in Rust, still supported (23.6).

Declarative macros

A macro_rules! in a Harsh file is a zone of Rust, copied as written; its calls are Harsh, square! n.

Procedural macros

A Rust proc-macro crate beside your Harsh crates, brought in with use.

Custom derive Macros

proc_macro_derive, called #[derive Name]; the Book's 23.6 has the Rust Book's HelloMacro used from Harsh.

Attribute-Like Macros

proc_macro_attribute, two streams, called #[route GET "/"] -- Rust's #[route(GET, "/")] -- replacing the item.

Function-Like Macros

proc_macro, called name! args.

Harsh DSLs (~)

Imported from Harsh libraries, Harsh throughout; the library's guide is the reference. The prelude's g~ and m~ are the first (pages 16 and 17).

Rust DSLs (!)

Imported from Rust libraries, called by Harsh's rules: vec! 1 2 3, #[derive Debug Serialize], #[tokio.main]; a stream that is a language of its own goes in braces, with Harsh in holes, @: … :@ (23.8).

A hole is an expression that calls on a variable, a function or a name the DSL binds, or uses an operator (. and :: too); a single word is exempt, {count}. Look in the strings' format slots, after a key's : in json!, in a splice ( … ) in Maud, after an attribute's : or =, in a brace pair of its own, after the DSL's for … in and if. The Book's 23.8 and the guide to macros have the full catalogue.

[dependencies]
serde_json = "1"
use serde_json.json

fn main$:
    let name = "Ada"
    let langs = vec! "Rust" "Harsh"
    let v = json! {{
        "name": @: name <- to_uppercase$ :@,
        "age": 36,
        "langs": langs,
        "count": @: langs <- len$ + 1 :@,
        "tags": [{ "id": 1, "main": @: langs <- first$ :@ }]
    }}
    println! "{v}"
{"age":36,"count":3,"langs":["Rust","Harsh"],"name":"ADA","tags":[{"id":1,"main":"Rust"}]}
[dependencies]
maud = "0.27"
use maud.(html, Markup)

fn page (title: &str) (items: &[&str]) -> Markup:
    html! {
        h1 { (title) }
        p.intro { "Items: " (@: items <- len$ :@) }
        a href=(@: format! "/items/{}" (items <- len$) :@) { "next" }
        ul {
            @for item in items {
                li { (@: item <- to_uppercase$ :@) }
            }
        }
        @if @: items <- is_empty$ :@ {
            p { "none" }
        }
    }

fn main$:
    println! "{}" (page "Fruit" (&["apple", "pear"]) <- into_string$)
<h1>Fruit</h1><p class="intro">Items: 2</p><a href="/items/2">next</a><ul><li>APPLE</li><li>PEAR</li></ul>

Precedence

Application binds tighter than anything but an atom: add 1 2 * 10 adds, then multiplies. The chain <- comes next, then ? and the prefix operators — so !v <- is_empty$ is "not empty", and (*r) <- len$ dereferences r before calling. Rust's operators follow in Rust's order, then the pipes. A negative or dereferenced argument is parenthesised: f (-1), f (*x). The full table is the Book's appendix Precedence.

struct Switch<'a>
    flag: &'a mut bool

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

fn double (x: i32) -> i32:
    x * 2

fn main$:
    let v = vec! 3 1 2
    // Application first: `add 1 2`, then `* 10`.
    println! "{}" (add 1 2 * 10)
    // A negative argument is parenthesised.
    println! "{}" (double (-4))
    // A chain before an operator: the length, plus one.
    println! "{}" (v <- len$ + 1)
    // A prefix operator takes the whole chain: not (v is empty).
    println! "{}" (!v <- is_empty$)
    // An application before a chain: `add 1 2`, then its method.
    println! "{}" (add 1 2 <- pow 2)
    // Dereference first, then the method: parenthesised.
    let r = &v
    println! "{}" ((*r) <- len$)
    // Through a field: `*s <- flag` is the bool the field points to.
    let mut on = false
    let mut s = Switch\ flag = &mut on
    *s <- flag = true
    println! "{on}"
    // Pipes, left to right; several values fill several parameters.
    println! "{}" (3 |> double |> double)
    println! "{}" (1 2 |> add)
    // To pipe a call's result, parenthesise the call.
    println! "{}" ((add 1 2) |> double)
    // Fewer values than parameters: a partial application.
    let add10 = 10 |> add
    println! "{}" (add10 5)
    // The backward pipe, right to left.
    println! "{}" (double <| double <| 5)
30
-8
4
true
9
3
true
12
3
6
15
20

8. Ownership and borrowing

This is the part of Rust that Harsh keeps entirely. Nothing here is a Harsh idea; the only thing to learn is how the spellings look.

8.1 Moving

fn consume (s: String) -> usize:
    s <- len$

fn main$:
    // A `String` owns its text. Passing it hands the ownership over.
    let a = String.from "hello"
    let n = consume a
    println! "{n}"
    // `a` is gone here: it was moved into `consume`.

    // `clone` makes a second owner.
    let b = String.from "world"
    let c = b <- clone$
    println! "{b} {c}"

    // Small copyable values are copied instead of moved.
    let x = 5
    let y = x
    println! "{x} {y}"
5
world world
5 5

A value that owns something — a String owns its text — has exactly one owner. Passing it to a function hands the ownership over, and the old name cannot be used afterwards. clone$ makes a second owner when you want one, and small copyable values such as integers are copied rather than moved.

Using a name after it has been moved is refused before the program runs:

fn consume (s: String):
    println! "{s}"

fn main$:
    let a = String.from "hello"
    consume a
    println! "{a}"
error[E0382]: borrow of moved value: `a`
  --> use_after_move.hrs:7:16
   |
 5 |     let a = String.from "hello"
   |         - move occurs because `a` has type `String`, which does not implement the `Copy` trait
 6 |     consume a
   |             - value moved here
 7 |     println! "{a}"
   |                ^ value borrowed here after move
   = note: consider changing this parameter type in function `consume` to borrow instead if owning the value isn't necessary (hrs 1:16)
   = help: consider cloning the value if the performance cost is acceptable (hrs 6:14)

The error arrives on the Harsh line that caused it, not on the generated Rust.

8.2 Borrowing

// A borrow reads without taking ownership.
fn length (s: &String) -> usize:
    s <- len$

// A mutable borrow may change what it points at.
fn shout (s: &mut String):
    s <- push_str "!"

fn main$:
    let mut greeting = String.from "hi"

    // `&` is an operator, so a borrowed argument is isolated.
    println! "{}" (length (&greeting))

    shout (&mut greeting)
    println! "{greeting}"

    // Many readers, or one writer, never both at once.
    let r1 = &greeting
    let r2 = &greeting
    println! "{r1} {r2}"

    let w = &mut greeting
    w <- push_str "?"
    println! "{greeting}"
2
hi!
hi! hi!
hi!?

&x borrows, &mut x borrows so it can change. The rule is one writer or any number of readers, never both, and the compiler checks it.

The Harsh part is only the parentheses: & is an operator, so a borrowed argument is isolated — length (&greeting), shout (&mut greeting).

8.3 Slices

// A slice borrows a run of something rather than the whole of it.
fn first_word (s: &str) -> &str:
    let bytes = s <- as_bytes$
    for (i, &b) in bytes <- iter$ <- enumerate$:
        if b == b' ':
            return &s[0..i]
    s

fn main$:
    let sentence = String.from "hello wide world"
    println! "{}" (first_word (&sentence))

    let numbers = [1, 2, 3, 4, 5]
    let middle = &numbers[1..4]
    println! "{:?} {}" middle (middle <- len$)
hello
[2, 3, 4] 3

A slice borrows a run of a value: &s[0..i] of a string, &numbers[1..4] of an array. Note &s[0..i] needs no extra parentheses when it is a return value, but would as an argument, by the same operator rule.

9. Generics and traits

9.1 Generics

// A generic function: one definition, many types.
fn largest<T: PartialOrd + Copy> (items: &[T]) -> T:
    let mut best = items[0]
    for &item in items <- iter$:
        if item > best:
            best = item
    best

// A generic type.
struct Pair<T>
    left: T
    right: T

impl<T: std.fmt.Display> Pair<T>
    fn show (&self):
        println! "({}, {})" (self <- left) (self <- right)

fn main$:
    println! "{}" (largest (&[1, 7, 3]))
    println! "{}" (largest (&[1.5, 0.2]))

    let p = Pair\ left = "a", right = "b"
    p <- show$
7
1.5
(a, b)

Generic parameters sit in <> after the name, with their bounds, exactly as in Rust. A bound long enough to be awkward goes in a bracketed where clause, which is Harsh's own spelling:

// A bound long enough to be awkward goes in a bracketed `where` clause.
// Brackets suppress layout, so the clause may span lines and its own `:`
// cannot be mistaken for a block opener.
fn describe_all<T> (items: &[T]) -> String
    [where T: std.fmt.Debug + Clone]:
    format! "{:?}" (items <- to_vec$)

fn main$:
    println! "{}" (describe_all (&[1, 2, 3]))
    println! "{}" (describe_all (&["a", "b"]))
[1, 2, 3]
["a", "b"]

The brackets are there because a bound contains a :, and a : at the end of a line opens a block. Brackets suppress layout, so the clause may span lines and its colons cannot be mistaken for openers.

9.2 Traits

trait Greet
    // A method with no body: whoever implements the trait writes it.
    fn name (&self) -> String

    // One with a body is a default, and may be overridden.
    fn hello (&self) -> String:
        format! "hello, {}" (self <- name$)

struct Cat
struct Dog\ called: String

impl Greet for Cat
    fn name (&self) -> String:
        String.from "cat"

impl Greet for Dog
    fn name (&self) -> String:
        self <- called <- clone$

    fn hello (&self) -> String:
        format! "woof, says {}" (self <- name$)

// Taking any type that implements the trait.
fn greet_all (who: &[&dyn Greet]):
    for one in who <- iter$:
        println! "{}" (one <- hello$)

fn main$:
    let d = Dog\ called = String.from "Rex"
    greet_all (&[&Cat, &d])
hello, cat
woof, says Rex

A trait declares methods; an impl … for … provides them. Both take no mark: the items follow the header. A method with a body in the trait is a default.

&dyn Greet is a trait object — any type implementing the trait, decided at run time. impl Greet as a parameter type is the other way, decided at compile time.

9.3 Operators are traits

use std.ops.Add

#[derive Debug Clone Copy]
struct Point
    x: i32
    y: i32

// Operators are traits, so `+` is written by implementing `Add`.
impl Add for Point
    type Output = Point

    fn add (self) (other: Point) -> Point:
        Point\ x = (self <- x) + (other <- x), y = (self <- y) + (other <- y)

fn main$:
    let a = Point\ x = 1, y = 2
    let b = Point\ x = 10, y = 20
    println! "{:?}" (a + b)
Point { x: 11, y: 22 }

+ is Add, * is Mul, == is PartialEq. Implementing the trait is what gives a type the operator, and type Output = … names what comes back.

This is why Harsh does not need operator syntax of its own for matrices or money or anything else: the operator is already a trait, and the trait is already implementable.

10. Errors

Harsh has no exceptions, because Rust has none. A function that can fail says so in its return type, and the caller has to deal with it.

10.1 Option

fn first_even (xs: &[i32]) -> Option<i32>:
    for &x in xs <- iter$:
        if x % 2 == 0:
            return Some x
    None

fn main$:
    println! "{:?}" (first_even (&[1, 3, 4]))
    println! "{:?}" (first_even (&[1, 3]))

    // Reading what is inside, safely.
    match first_even (&[5, 6])\
        Some n => println! "found {n}"
        None => println! "found nothing"

    // Or with a default.
    println! "{}" (first_even (&[1]) <- unwrap_or 0)

    // `?` on an Option leaves the function early when it is None.
    println! "{:?}" (doubled_first (&[4, 5]))
    println! "{:?}" (doubled_first (&[1, 5]))

fn doubled_first (xs: &[i32]) -> Option<i32>:
    let n = first_even xs?
    Some (n * 2)
Some(4)
None
found 6
0
Some(8)
None

Option<T> is either Some value or None. match reads it; unwrap_or gives a default; ? leaves the function early when it is None.

Note Some n and Some (n * 2): constructing a variant is an application like any other, so the payload is juxtaposed and an expression is isolated.

10.2 Result

fn parse_age (text: &str) -> Result<u32, String>:
    // `map_err` turns one error into another.
    let n: u32 =
        text <- trim$
             <- parse$
             <- map_err (|e| format! "{text:?} is not a number ({e})")?
    if n > 150:
        return Err (format! "{n} is too old")
    Ok n

fn main$:
    for text in ["31", " 7 ", "abc", "900"]:
        match parse_age text\
            Ok n => println! "age {n}"
            Err why => println! "rejected: {why}"
age 31
age 7
rejected: "abc" is not a number (invalid digit found in string)
rejected: 900 is too old

Result<T, E> is Ok value or Err why. ? is the same shorthand: it returns the error to the caller, converting it if the types allow.

The chain in parse_age is worth reading slowly — text <- trim$ <- parse$ <- map_err (…)? — because it shows the two mechanisms side by side: <- walks the chain, and ? at the end takes the error out of it.

10.3 An error type of your own

use std.fmt

// An error of one's own is a type with a `Display`.
#[derive Debug]
enum ConfigError
    Missing String
    Bad
        key: String
        value: String

impl fmt.Display for ConfigError
    fn fmt (&self) (f: &mut fmt.Formatter) -> fmt.Result:
        match self\
            ConfigError.Missing key => write! f "{key} is missing"
            ConfigError.Bad\ key, value => write! f "{key} cannot be {value}"

// An impl with no items of its own keeps Rust's braces: there is no body
// to put beneath it.
impl std.error.Error for ConfigError {}

fn port (raw: Option<&str>) -> Result<u16, ConfigError>:
    let Some text = raw else:
        return Err (ConfigError.Missing (String.from "port"))
    text <- parse$ <- map_err |_|:
        ConfigError.Bad\ key = String.from "port", value = String.from text

fn main$:
    for raw in [Some "8080", Some "wat", None]:
        match port raw\
            Ok n => println! "port {n}"
            Err e => println! "error: {e}"
port 8080
error: port cannot be wat
error: port is missing

An error is a type with a Display. Giving it std.error.Error as well lets it travel as a Box<dyn Error>, which is what a program that has several kinds of failure usually returns.

Two Harsh details in that program. An impl with no items of its own keeps Rust's braces — impl std.error.Error for ConfigError {} — since there is no body to put beneath it. And a record variant is matched with the \ it was declared with: ConfigError.Bad\ key, value.

11. Collections

11.1 Vectors

fn main$:
    // `vec!` builds one; brackets are Rust's and pass through.
    let mut v = vec! 1 2 3
    v <- push 4

    println! "{:?} {}" v (v <- len$)
    println! "{}" v[0]

    // `get` returns an Option rather than panicking.
    println! "{:?} {:?}" (v <- get 1) (v <- get 99)

    // Iterating, and building a new one.
    let doubled: Vec<i32> =
        v <- iter$
          <- map (|n| n * 2)
          <- collect$
    println! "{:?}" doubled

    let total: i32 = v <- iter$ <- sum$
    println! "{total}"
[1, 2, 3, 4] 4
1
Some(2) None
[2, 4, 6, 8]
10

vec! 1 2 3 builds one: a macro is applied like a function, one argument per value. vec! { 0; 4 } repeats one value, four times -- that stream is not a list of values, so it goes in braces and reaches the macro as written.

v[0] indexes and panics if it is out of range; v <- get 0 returns an Option instead.

11.2 Maps

use std.collections.HashMap

fn main$:
    let mut ages = HashMap.new$
    ages <- insert "Ada" 36
    ages <- insert "Alan" 41

    // Looking up gives an Option.
    match ages <- get "Ada"\
        Some n => println! "Ada is {n}"
        None => println! "no Ada"

    // `entry` inserts only when the key is absent.
    ages <- entry "Grace" <- or_insert 45
    ages <- entry "Ada" <- or_insert 0

    // A HashMap has no order, so sort before printing.
    let mut pairs: Vec<_> = ages <- iter$ <- collect$
    pairs <- sort$
    println! "{:?}" pairs
Ada is 36
[("Ada", 36), ("Alan", 41), ("Grace", 45)]

HashMap.new$, then insert, get, entry. A map has no order, so a program that prints one should sort first — which is why the last two lines collect into a Vec and sort it.

11.3 Strings

fn main$:
    // `&str` borrows; `String` owns.
    let borrowed = "hello"
    let mut owned = String.from borrowed
    owned <- push_str ", world"
    println! "{owned}"

    // Slicing is by bytes, so it must fall on a character boundary.
    println! "{}" (&owned[0..5])

    // Splitting, trimming, joining.
    let csv = " a,b,c "
    let parts: Vec<&str> =
        csv <- trim$
            <- split ','
            <- collect$
    println! "{:?}" parts
    println! "{}" (parts <- join " + ")

    // Characters, not bytes, when you mean characters.
    println! "{}" ("héllo" <- chars$ <- count$)
hello, world
hello
["a", "b", "c"]
a + b + c
5

&str borrows text; String owns it. Indexing is by byte and must land on a character boundary, so counting characters is chars$ <- count$ rather than len$.

12. Modules

// A module groups items. Its body follows the header, with no mark.
mod shapes
    // Items are private to the module unless they say `pub`.
    pub struct Circle
        pub radius: f64

    impl Circle
        pub fn area (&self) -> f64:
            3.14159 * (self <- radius) * (self <- radius)

    pub mod units
        pub const NAME: &str = "cm"

// `use` brings a path into scope; `.` is the path separator.
use shapes.Circle
use shapes.units.NAME

fn main$:
    let c = Circle\ radius = 2.0
    println! "{:.2} square {}" (c <- area$) NAME

    // Or written out in full, without a `use`.
    println! "{}" shapes.units.NAME
12.57 square cm
cm

A mod groups items, and its body follows the header with no mark, like every other item body. Everything inside is private unless it says pub.

. is the path separator, so Rust's shapes::units::NAME is shapes.units.NAME, and use shapes.Circle brings it into scope. A grouped import is written with parentheses: use std.io.(Read, Write).

Privacy is checked, so reaching a field that is not pub is refused:

mod counter
    pub struct Counter
        // Not `pub`: only this module may touch it.
        value: i32

    impl Counter
        pub fn new$ -> Counter:
            Counter\ value = 0

fn main$:
    let c = counter.Counter.new$
    println! "{}" (c <- value)
error[E0616]: field `value` of struct `Counter` is private
  --> privacy.hrs:12:25
   |
12 |     println! "{}" (c <- value)
   |                         ^^^^^ private field

In a real project the modules live in files rather than in one: src/main.hrs plus src/shapes.hrs, and mod shapes in the main file. hrs build transpiles the tree and cargo compiles it.

13. Threads and channels

use std.thread
use std.sync.mpsc

fn main$:
    // A thread takes a closure. `move` hands it what it captures.
    let handle = thread.spawn move ||:
        let mut total = 0
        for i in 1..=10:
            total += i
        total

    // `join` waits for it and gives back what it returned.
    let total = handle <- join$ <- unwrap$
    println! "the thread counted {total}"

    // A channel carries values between threads.
    let (tx, rx) = mpsc.channel$
    for id in 0..3:
        let tx = tx <- clone$
        // Bound to `_`: the block's last expression is its value, and here
        // the handle is not wanted, so the binding discards it.
        thread.spawn move ||:
            tx <- send (id * id) <- unwrap$
        ()
    drop tx

    let mut got: Vec<i32> = rx <- iter$ <- collect$
    got <- sort$
    println! "{:?}" got
the thread counted 55
[0, 1, 4]

thread.spawn takes a closure, and move hands it what it captured. Note the spelling: thread.spawn move ||: with the body beneath — a closure is a trailing argument that owns its block, and move before the bars is part of it.

join$ waits and gives back whatever the closure returned.

A channel is a pair: tx to send, rx to receive. Every thread gets its own clone of tx, and the original is dropped so the receiver knows when the last one is gone.

One Harsh detail worth the line it costs: the () after thread.spawn …. A block's value is its last line, and without the () the loop body would be trying to return a JoinHandle, and rustc would say so. The () says the body is worth nothing; it goes on the line after the closure's body, at the call's own column. (let _ = thread.spawn … compiles too, but means something else: binding to _ drops the value at once — what you want for a guard you mean to release, not for a value you merely ignore.)

14. Tests

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

fn divide (a: i32) (b: i32) -> Result<i32, String>:
    if b == 0:
        return Err (String.from "divide by zero")
    Ok (a / b)

// A test module is compiled only when testing.
#[cfg (test)]
mod tests
    use super.*

    #[test]
    fn addition_works$:
        assert_eq! (add 2 2) 4

    #[test]
    fn division_reports_its_error$:
        assert! (divide 1 0 <- is_err$)
        assert_eq! (divide 10 2) (Ok 5)

    #[test]
    fn a_message_of_your_own$:
        let n = add 2 2
        assert! (n > 0) "expected a positive number, got {}" n
$ hrs test
running 3 tests
test tests::a_message_of_your_own ... ok
test tests::addition_works ... ok
test tests::division_reports_its_error ... ok

test result: ok. 3 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished

A test is a function marked #[test], usually inside a #[cfg (test)] mod tests, which is compiled only when testing. use super.* brings in what the file above defines.

Three things read differently from Rust, and all three are the ordinary rules:

  • #[cfg (test)] — an attribute is an application, so its argument is isolated in parentheses when it is not a single atom.
  • fn addition_works$ — a test takes no parameters, so it takes $.
  • assert_eq! (add 2 2) 4 — a macro's arguments juxtapose, and add 2 2 is an application, so it is isolated.

hrs test runs them, passing the arguments through to cargo.

15. The tools

Harsh is a transpiler, so the tools are a thin layer over cargo's.

hrs new NAME          a project: Cargo.toml, src/main.hrs, .gitignore
hrs run               transpile what changed, then cargo run
hrs build             transpile, then cargo build
hrs test              transpile, then cargo test
hrs check             transpile, then cargo check
hrs lint              the same, through clippy, with its notes remapped
hrs fmt               lay out the Harsh in the canonical form
hrs watch             rebuild as files change
hrs expand FILE       the file with its Harsh macros unfolded
hrs -c CMD            cargo CMD in export/: hrs -c publish for crates.io
hrs -- PROGRAM        any program, run in export/: hrs -- dx serve

A project keeps its Harsh in src/**.hrs, and its Cargo.toml is cargo's, unchanged. Each build writes the project as Rust to export/ -- its Cargo.toml, src/ transpiled, the other files beside them -- and cargo builds it there:

myapp/
    Cargo.toml  Hrs.toml  src/main.hrs
    export/     Cargo.toml  src/main.rs  target/
    .hrs/       the source maps

hrs new puts /export and /.hrs in .gitignore: both are written again from the .hrs files.

// This is what `hrs new` writes for you, and what `hrs run` builds.
fn main$:
    let args: Vec<String> = std.env.args$ <- collect$
    let name =
        if (args <- len$) > 1:
            args[1] <- clone$
        else:
            String.from "world"
    println! "hello, {name}"
hello, world

Every diagnostic comes back on the .hrs line and column that produced it, not on the generated Rust, because the transpiler writes a source map and the driver runs cargo's output back through it. That is true of rustc's errors, its suggestions, and clippy's lints alike.

hrs-from goes the other way, turning Rust into Harsh — which is how an existing crate is brought across.

16. Generator comprehensions

16.1 g~

fn main$:
    // A comprehension: the value, then where it comes from.
    let squares: Vec<i32> = (g~ x * x for x in 1..6) <- collect$
    println! "{:?}" squares

    // `if`s filter; each belongs to the `for` it follows.
    let odd_big: Vec<i32> = (g~ x for x in 0..20 if x % 2 == 1 if x > 10) <- collect$
    println! "{:?}" odd_big

    // Several `for`s nest, and an inner condition sees the outer name.
    let pairs: Vec<(i32, i32)> = (g~ (x, y) for x in 1..4 for y in 1..4 if y > x) <- collect$
    println! "{:?}" pairs
[1, 4, 9, 16, 25]
[11, 13, 15, 17, 19]
[(1, 2), (1, 3), (2, 3)]

g~ is Harsh's comprehension, and it is always available — it lives in the prelude, so there is nothing to use. It reads the way the idea reads: the value first, then for a pattern in an iterable, then any conditions.

Each if belongs to the for it follows, so a condition can use every name bound up to that point — if y > x sees both — and a condition on the outer loop filters before the inner one runs.

g~ produces an iterator, and it is lazy: nothing is computed until something asks for the values. for means what it means everywhere in the language, so it takes the iterable by value; write xs <- iter$ to borrow instead.

16.2 Conditions, and several fors

fn main$:
    // `if`s filter, and every `if` belongs to the `for` before it.
    let picked: Vec<i32> = (g~ x for x in 0..30 if x % 3 == 0 if x % 2 == 1) <- collect$
    println! "{picked:?}"

    // A condition sees every name bound so far: `y > x` uses both.
    let rising: Vec<(i32, i32)> = (g~ (x, y) for x in 1..4 for y in 1..4 if y > x) <- collect$
    println! "{rising:?}"
[3, 9, 15, 21, 27]
[(1, 2), (1, 3), (2, 3)]

Each if belongs to the for before it, so a condition sees every name bound up to that point — y > x uses both — and runs as early as it can.

16.3 Where a condition may go

fn main$:
    let triples =
        list~ (a, b, c)
            for a in 1..20        // only a is available for the condition
            for b in a..20        // only a, b are available for the condition
            for c in b..20        // a, b, c are available for the condition
            if a * a + b * b == c * c
    println! "{triples:?}"
[(3, 4, 5), (5, 12, 13), (6, 8, 10), (8, 15, 17), (9, 12, 15)]

Every for makes one more name available, and a condition can use only the names bound up to its place — so a test goes after the last for whose name it uses. Attached to for a, the same test cannot see b:

fn main$:
    // The test needs b and c, but it is attached to `for a`,
    // where neither exists yet.
    let triples =
        list~ (a, b, c)
            for a in 1..20 if a * a + b * b == c * c
            for b in a..20
            for c in b..20
    println! "{triples:?}"
error[E0425]: cannot find value `b` in this scope
  --> too_early.hrs:6:39
   |
 6 |             for a in 1..20 if a * a + b * b == c * c
   |                                       ^
   = help: a local variable with a similar name exists (hrs 6:39)

error[E0425]: cannot find value `b` in this scope
  --> too_early.hrs:6:43
   |
 6 |             for a in 1..20 if a * a + b * b == c * c
   |                                           ^
   = help: a local variable with a similar name exists (hrs 6:43)

error[E0425]: cannot find value `c` in this scope
  --> too_early.hrs:6:48
   |
 6 |             for a in 1..20 if a * a + b * b == c * c
   |                                                ^
   = help: a local variable with a similar name exists (hrs 6:48)

error[E0425]: cannot find value `c` in this scope
  --> too_early.hrs:6:52
   |
 6 |             for a in 1..20 if a * a + b * b == c * c
   |                                                    ^
   = help: a local variable with a similar name exists (hrs 6:52)

16.4 Collecting: list~, set~, dict~

use std.collections.HashMap
use std.collections.HashSet

fn main$:
    // `g~` is lazy -- a plain iterator. The shorthands collect it.
    let v = list~ x * 10 for x in 1..4
    println! "{:?}" v

    let remainders: HashSet<i32> = set~ x % 3 for x in 0..10
    let mut seen: Vec<_> = remainders <- into_iter$ <- collect$
    seen <- sort$
    println! "{:?}" seen

    // `dict~` takes `key => value`.
    let lengths: HashMap<&str, usize> = dict~ w => (w <- len$) for w in ["sun", "moon"]
    let mut pairs: Vec<_> = lengths <- into_iter$ <- collect$
    pairs <- sort$
    println! "{:?}" pairs
[10, 20, 30]
[0, 1, 2]
[("moon", 4), ("sun", 3)]

The shorthands collect a comprehension into a Vec, a HashSet or a HashMap. dict~ takes its entry as key => value. Or keep g~ and let the type decide — let v: Vec<_> = (g~ …) <- collect$ — which is the same thing written out.

16.5 Laying one out

fn main$:
    // Every line indented beneath the call belongs to it, so a long one is
    // laid out one clause per line. Pythagorean triples:
    let triples =
        list~ (a, b, c)
            for a in 1..20
            for b in a..20
            for c in b..20 if a * a + b * b == c * c
    println! "{:?}" triples
[(3, 4, 5), (5, 12, 13), (6, 8, 10), (8, 15, 17), (9, 12, 15)]

A call takes the rest of its line and every line indented beneath it, so a comprehension with several clauses is written one clause per line. Nothing changes but the layout: the same tokens reach the macro either way.

If you define a macro of your own called g (or list, set, dict), yours wins in that file. The prelude's is still there as hrs_std.g~.

16.6 Lazy, and by value

fn main$:
    // `g~` is an iterator: nothing runs until something asks for values.
    // So an endless source is fine, as long as something stops asking.
    let first: Vec<u64> =
        (g~ n * n for n in 1.. if n % 7 == 3) <- take 4 <- collect$
    println! "{first:?}"

    // `for` takes its iterable by value, as it does everywhere in Rust.
    // Borrow to keep the collection.
    let words = vec! (String.from "pipe") (String.from "matrix")
    let lengths: Vec<usize> = (g~ (w <- len$) for w in (words <- iter$)) <- collect$
    println! "{lengths:?} -- and still {words:?}"
[9, 100, 289, 576]
[4, 6] -- and still ["pipe", "matrix"]

g~ is an iterator: nothing runs until something asks, so an endless source is fine when something stops asking. for takes its iterable by value; give it a borrow, (words <- iter$), to keep the collection.

16.7 What it stands for

fn main$:
    let wanted: Vec<i32> =
        (g~ x * 10 + y
            for x in (0..6) if x % 2 == 0 if x > 0
            for y in (0..6) if y > x if y % 2 == 1
        ) <- collect$

    // The same by hand: one `flat_map` per `for`, each level's conditions
    // folded into one test, and the outer level flattened.
    let inner =
        |x: i32| (0..6) <- into_iter$
            <- flat_map (move |y| ((true && (y > x) && (y % 2 == 1)) <- then (|| x * 10 + y)))
    let written: Vec<i32> =
        (0..6) <- into_iter$
               <- flat_map (move |x| ((true && (x % 2 == 0) && (x > 0)) <- then (|| inner x)))
               <- flatten$
               <- collect$

    println! "{wanted:?} {}" (wanted == written)
[23, 25, 45] true

A generator comprehension is a chain written another way: each for a flat_map, its conditions folded into one test under then, every level but the innermost flattened. The flatten$ is part of the generator, so it is never written by hand; the collect$ is not, because a generator is lazy — write it, or use list~. Chapter 15 of the Book shows the whole expansion.

fn main$:
    let scores = vec! 72 45 91 60 88

    // A comprehension...
    let a: Vec<i32> = (g~ s + 5 for s in (scores <- iter$) if *s >= 60) <- collect$
    // ...and the chain it stands for.
    let b: Vec<i32> =
        scores <- iter$
               <- filter (|s| **s >= 60)
               <- map (|s| s + 5)
               <- collect$
    println! "{a:?} {}" (a == b)
[77, 96, 65, 93] true

17. Matrices

Harsh's matrices are Julia's, for anyone who knows Julia: the same literal, the same meaning for *, the same errors. They live in hrs_std, Harsh's standard library, which a project adds to its dependencies once. The literals themselves, m~ and v~, need no use: they are part of the language.

17.1 Writing them

# Hrs.toml
[dependencies]
hrs_std = "0.1"
fn main$:
    // A matrix: spaces between entries, `;` between rows.
    let a = m~ [1 2 3; 4 5 6]
    println! "{a}"
    // Rows may be blocks of their own, stacked with `;`.
    let b = m~ [[1 2 3]; [4 5 6]]
    println! "{}" (a == b)

    // A vector is a column: commas, or one entry per line.
    let v = v~ [1.5, 2.5, 3.5]
    println! "{v}"

    // A bracket inside is a block. Side by side, columns make a matrix.
    let c = m~ [[1, 4] [2, 5] [3, 6]]
    println! "{}" (a == c)
2×3 Matrix<i32>:
 1 2 3
 4 5 6
true
3-element Vector<f64>:
 1.5
 2.5
 3.5
true

Three rules make every matrix literal, and they compose. A space puts things side by side. A ; puts them one above another — and so does a line break, so a matrix may be written one row per line, as it reads. A comma makes the entries of a vector, which is a column.

A bracket inside the literal is a block built by the same rules, so [[1, 4] [2, 5] [3, 6]] is three columns side by side — the same matrix as [1 2 3; 4 5 6]. What the literal refuses, it refuses with Julia's reason: m~ [[1 2], [3 4]] is two matrices in a vector, not one matrix, because a comma never joins; and v~ [1 2 3] is a row, which is a matrix, not a vector.

A matrix prints its shape and its element type, as Julia does.

17.2 Arithmetic

# Hrs.toml
[dependencies]
hrs_std = "0.1"
fn main$:
    let a = m~ [1.0 2.0; 3.0 4.0]
    let b = m~ [0.0 1.0; 1.0 0.0]
    let x = v~ [1.0, 1.0]

    // One `*`, three meanings, chosen from what it multiplies.
    println! "{}" (&a * &b)        // the matrix product
    println! "{}" (2.0 * (&a + &b))  // scaling a sum
    println! "{}" (&a * &x)        // a matrix times a vector is a vector

    println! "{:?} {}" (a <- size$) (a <- transpose$)
2×2 Matrix<f64>:
 2 1
 4 3
2×2 Matrix<f64>:
 2 6
 8 8
2-element Vector<f64>:
 3
 7
(2, 2) 2×2 Matrix<f64>:
 1 3
 2 4

* between two matrices is the matrix product; between a number and a matrix it is scaling; between a matrix and a vector it gives a vector. The compiler chooses, from what is being multiplied — nothing has to be spelled out.

The & is ownership, Rust's rule: &a * &b borrows both, so a and b are still there on the next line. Without it, the product would consume them.

17.3 The identity

# Hrs.toml
[dependencies]
hrs_std = "0.1"
use hrs_std.(UniformScaling, I)

fn main$:
    let a = m~ [1 2; 3 4]
    let u = UniformScaling 2
    // As in Julia: `u` is 2 times an identity of whatever size is needed.
    println! "{}" (&a + u)
    println! "{}" (&a * u)
    println! "{}" (&a + I)
2×2 Matrix<i32>:
 3 2
 3 6
2×2 Matrix<i32>:
 2 4
 6 8
2×2 Matrix<i32>:
 2 2
 3 5

UniformScaling k is k times an identity of whatever size the other side needs, and I is the identity itself — Julia's own names.

17.4 Joining matrices

# Hrs.toml
[dependencies]
hrs_std = "0.1"
fn main$:
    let a = m~ [1 2; 3 4]
    // Any matrix is a block, so the literal joins matrices as it joins numbers.
    println! "{}" (m~ [(&a) (&a)])
    println! "{}" (m~ [(&a); (&a)])
2×4 Matrix<i32>:
 1 2 1 2
 3 4 3 4
4×2 Matrix<i32>:
 1 2
 3 4
 1 2
 3 4

A block may be any matrix, so the literal that builds a matrix from numbers also joins matrices: side by side with a space, stacked with a ;. Each block is isolated in parentheses, (&a), as any argument with an operator is.

17.5 Solving

# Hrs.toml
[dependencies]
hrs_std = "0.1"
fn main$:
    // 2x + y = 5 and x + y = 3, as a matrix and a vector.
    let a = m~ [2.0 1.0; 1.0 1.0]
    let b = v~ [5.0, 3.0]
    // Julia's `a \ b`.
    let x = a <- solve (&b)
    println! "x = {}, y = {}" x[0] x[1]

    println! "det {}" (a <- det$)
    println! "{}" (a <- inv$)

    let v = v~ [3.0, 4.0]
    println! "length {}, dot {}" (v <- norm$) (v <- dot (&v))
x = 2, y = 1
det 1
2×2 Matrix<f64>:
 1 -1
 -1 2
length 5, dot 25

a <- solve (&b) is Julia's a \ b: the x with a * x == b. det$, inv$, norm$ and dot carry Julia's names. Indexes start at 0, as for every collection in Harsh.

solve and inv stop the program on a singular matrix, as Julia does. When the matrix comes from data you have not checked, try_solve and try_inv return a Result instead — Err (LinAlgError.Singular), or Err (LinAlgError.DimensionMismatch msg) — as RefCell's try_borrow pairs with borrow:

# Hrs.toml
[dependencies]
hrs_std = "0.1"
use hrs_std.LinAlgError

fn main$:
    let a = m~ [2.0 1.0; 1.0 3.0]
    let flat = m~ [1.0 2.0; 2.0 4.0]
    let b = v~ [5.0, 10.0]
    // Sure of the matrix: Julia's way -- the answer, or a panic.
    let x = a <- solve (&b)
    println! "x = {:.1} {:.1}" x[0] x[1]
    // Not sure: the caller decides what a failure means.
    for m in [&a, &flat]:
        match m <- try_solve (&b)\
            Ok x => println! "solved: {:.1} {:.1}" x[0] x[1]
            Err LinAlgError.Singular => println! "no single solution: the matrix is singular"
            Err e => println! "{e}"
    let short = v~ [1.0, 2.0, 3.0]
    if let Err e = a <- try_solve (&short):
        println! "{e}"
x = 1.0 3.0
solved: 1.0 3.0
no single solution: the matrix is singular
DimensionMismatch: matrix has 2 rows, right-hand side has length 3

17.6 Fitting a line

# Hrs.toml
[dependencies]
hrs_std = "0.1"
use hrs_std.Matrix

fn main$:
    // Hours studied, and the score each student got.
    let hours = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]
    let score = v~ [52.0, 55.0, 61.0, 64.0, 70.0, 73.0]

    // The design matrix, one row per student: a 1 for the intercept,
    // then the hours. A comprehension builds the rows.
    let x = Matrix.from_rows (list~ (vec! 1.0 h) for h in hours)

    // Julia's `β = X \ y`: for a tall matrix, the least-squares fit.
    let beta = x <- solve (&score)
    println! "score = {:.2} + {:.2} × hours" beta[0] beta[1]

    let fitted = &x * &beta
    println! "residual {:.2}" ((&score - &fitted) <- norm$)
    println! "7 hours: {:.1}" (beta[0] + beta[1] * 7.0)
warning[unused_parens]: unnecessary parentheses around closure body
  --> /tmp/harsh-by-example/projects/17_matrices/regression/src/main.hrs:10:38
   |
10 |     let x = Matrix.from_rows (list~ (vec! 1.0 h) for h in hours)
   |                                      ^
12 |     // Julia's `β = X \ y`: for a tall matrix, the least-squares fit.
   |         ^
   = note: in the expansion of `hrs_std.g~` called at /tmp/harsh-by-example/projects/17_matrices/regression/src/main.hrs:12:56
12 |     // Julia's `β = X \ y`: for a tall matrix, the least-squares fit.
   = note: `#[warn(unused_parens)]` (part of `#[warn(unused)]`) on by default
   = help: remove these parentheses (hrs 10:38)

warning[unused_parens]: unnecessary parentheses around closure body
  --> /tmp/harsh-by-example/projects/17_matrices/regression/src/main.hrs:10:37
   |
10 |     let x = Matrix.from_rows (list~ (vec! 1.0 h) for h in hours)
   |                                     ^
   |                                                ^
   = help: remove these parentheses (hrs 10:37)

score = 47.20 + 4.37 × hours
residual 1.76
7 hours: 77.8
hrs: 0 error(s), 2 warning(s)

With more rows than columns, solve returns the least-squares fit, as Julia's X \ y does: linear regression in one line, with the design matrix built by a comprehension. Chapter 16 of the Book explains each step.

17.7 Slicing

# Hrs.toml
[dependencies]
hrs_std = "0.1"
fn main$:
    let mut a = m~ [1 2 3; 4 5 6; 7 8 9]
    // The method copies, as Julia's `a[1:2, :]` does. `..` alone is Julia's `:`.
    println! "{}" (a <- slice (0..2) (..))
    // An axis taken by a number is dropped: a row is a vector.
    println! "{}" (a <- slice 1 (..))
    // A view borrows, as Julia's `view(a, 2:3, 1:2)`: nothing copied.
    let window = a <- view (1..) (..=1)
    println! "{}" window
    let corner = window <- copy$
    println! "{}" corner
    // An element by its index; a whole row through a view that writes.
    a[1, 1] = 50
    a <- view_mut 2 (..) <- fill 0
    println! "{}" a
2×3 Matrix<i32>:
 1 2 3
 4 5 6
3-element Vector<i32>:
 4
 5
 6
2×2 view of Matrix<i32>:
 4 5
 7 8
2×2 Matrix<i32>:
 4 5
 7 8
3×3 Matrix<i32>:
 1 2 3
 4 50 6
 0 0 0

Julia writes a[1:2, :]. Harsh keeps its own ranges -- 0-based, the end left out, as for every collection -- and .. alone is Julia's :. There are two ways to take a part. a <- slice (0..2) (..) copies, as Julia's a[1:2, :] does. a <- view (0..2) (..) borrows, as Julia's view(a, 1:2, :): a window onto a, guarded by the borrow checker like any borrow, and a value you can name, print or use in .*; <- copy$ makes it a matrix of its own. An axis taken by a number is dropped, so slice 1 (..) and view 1 (..) are a row, read as a vector. To write, a[1, 1] = 50 sets an element, and view_mut is the view that writes through: a <- view_mut 2 (..) <- fill 0.

An index takes its axes with a comma, a[i, j]; the parenthesised a[(i, j)] means the same and is what the comma stands for. An index is one element: a part of a matrix is a slice or a view.

17.8 Broadcasting

# Hrs.toml
[dependencies]
hrs_std = "0.1"
fn relu (x: f64) -> f64:
    x <- max 0.0

fn main$:
    let a = m~ [1.0 -2.0; -3.0 4.0]
    let b = m~ [10.0 20.0; 30.0 40.0]
    let row = m~ [100.0 200.0]
    // `*` is the matrix product; `.*` multiplies element by element.
    println! "{}" (&a * &b)
    println! "{}" (&a .* &b)
    // Shapes stretch as in Julia: a number, a row, a column.
    println! "{}" (&a .* 2.0 .+ &row)
    // `f<>` applies `f` to each element: Julia's `f.(a)`.
    println! "{}" (relu<> a)
    println! "{}" (f64.powf<> a 2.0)
    println! "{}" ((|x, y| x > y)<> a b)
    // It pipes, and nothing above consumed `a`.
    println! "{}" (a |> relu<> |> f64.sqrt<>)
2×2 Matrix<f64>:
 -50 -60
 90 100
2×2 Matrix<f64>:
 10 -40
 -90 160
2×2 Matrix<f64>:
 102 196
 94 208
2×2 Matrix<f64>:
 1 0
 0 4
2×2 Matrix<f64>:
 1 4
 9 16
2×2 Matrix<bool>:
 false false
 false false
2×2 Matrix<f64>:
 1 0
 0 2

A dot before an operator applies it element by element: .*, .+, .-, ./, Julia's own spellings. Shapes stretch as Julia stretches them -- along an axis two lengths must be equal, or one of them 1 -- so a number, a row or a column combines with a matrix. Precedence is the operator's own: x + a .* b multiplies first.

f<> is apply to each, Julia's f.(a): a mark written tight against a function, as $ and ! are, saying how it is applied. It takes up to three arguments, borrows them, works on a closure in parentheses, and pipes. With it the operators that have no dotted form are ordinary functions: f64.powf<> a 2.0 is Julia's a .^ 2, and a comparison gives a matrix of bool. Beneath it is a method, a <- map f, as slice is beneath the index.

17.9 When sizes do not fit

# Hrs.toml
[dependencies]
hrs_std = "0.1"
use hrs_std.UniformScaling

fn main$:
    let b = m~ [1 2 3; 4 5 6]
    // Adding a scaled identity needs a square matrix, and this is 2×3.
    println! "{}" (&b - (UniformScaling 2))
DimensionMismatch: matrix is not square: dimensions are (2, 3)

A matrix's size is a value, known when the program runs, not a type. So a mismatch is caught then, and reported in Julia's words: DimensionMismatch, with the sizes that did not fit.