A Harsh companion to Rust Design Patterns — its idioms, patterns and anti-patterns, in the same order, with every program written in Harsh and built and run to produce the output you see.
A design pattern is a solution that has worked before, to a problem that keeps coming back: a way to arrange code that other programmers will recognise. Rust has its own, because it is not an object-oriented language: ownership, the borrow checker, traits and closures make some classic patterns unnecessary and call for others. Rust Design Patterns, a community book, collects them in three families — idioms, the habits of the community; design patterns, arrangements that solve a recurring problem; and anti-patterns, arrangements that look like solutions and create problems.
Harsh is Rust with another layout, so every one of them holds in Harsh as it is. What this companion adds is how each one is written in Harsh, and where Harsh changes the picture: pipes and partial application make several patterns a line long, comprehensions replace some loops outright, and the layout rules make a builder or a command list read top to bottom. Where Harsh changes nothing, the page says so and keeps short.
How to read it
Each page gives the idea in a few sentences, a program in Harsh with its real output, what Harsh changes, and a link to the original page — the place for the full discussion, the trade-offs and the history, which this companion does not repeat. If you have not read The Harsh Programming Language yet, read it first: this book assumes you can read Harsh.
The book follows the original's order: the idioms; the design patterns — behavioural, creational, structural, and for foreign functions; the anti-patterns; functional programming; and, to close, the design principles behind them all.
Rust Design Patterns is by its contributors, under the Mozilla Public License 2.0, as is this companion; see ATTRIBUTION.md.
Idioms
Idioms are the community's habits: small, local choices that make Rust code read as Rust. They are not rules — break one when there is a reason — but code that follows them is easier for everyone else to read. The original chapter: Idioms.
2.1 Use borrowed types for arguments
Original: Use borrowed types for arguments
Take the most general borrowed form a function can work with: &str rather
than &String, &[T] rather than &Vec<T>, &T rather than &Box<T>. The
caller's &String coerces to &str on its own, so the function accepts more
— a string literal, a slice of another string — at no cost.
// `&str`, not `&String`: every string the caller has will do.
fn three_vowels (word: &str) -> bool:
let mut count = 0
for c in word <- chars$:
match c\
'a' | 'e' | 'i' | 'o' | 'u' => count += 1
_ => count = 0
if count >= 3:
return true
false
fn main$:
let owned = String.from "Ferris"
let sentence = "Once upon a time, there was a friendly curious crab named Ferris"
println! "{}: {}" owned (three_vowels (&owned))
for word in sentence <- split ' ':
if three_vowels word:
println! "{word} has three consecutive vowels!"
Ferris: false
curious has three consecutive vowels!
In Harsh: nothing changes; the coercion is Rust's. An argument that is not a single atom takes
parentheses: three_vowels (&owned).
2.2 Concatenating Strings with format!
Original: Concatenating Strings with format!
To build a string from pieces, format! is usually clearer than a series of
push_str calls: the shape of the result is visible at once. For a string
built in a loop, or where every allocation counts, pushing into one String
remains the faster choice.
fn greet (name: &str) -> String:
// The shape of the result, at a glance.
format! "Hello {name}!"
fn main$:
println! "{}" (greet "Harsh")
// Built piece by piece, where the pieces come from a loop.
let mut line = String.new$
for part in ["a", "b", "c"]:
line <- push_str part
println! "{line}"
Hello Harsh!
abc
In Harsh: format! takes its arguments by juxtaposition, and names can
sit inside the braces, so most calls need no arguments after the string.
2.3 Constructor
Original: Constructor
Rust has no constructors in the language: by convention, an associated
function new builds a value. When a type has an obvious default, implement
Default too, and let new call it or be it.
#[derive Debug]
pub struct Second
value: u64
impl Second
// By convention, `new` builds the value.
pub fn new (value: u64) -> Self:
Self\ value = value
pub fn value (&self) -> u64:
self <- value
impl Default for Second
fn default$ -> Self:
Self\ value = 0
fn main$:
let s = Second.new 42
println! "{}" (s <- value$)
println! "{:?}" (Second.default$)
42
Second { value: 0 }
In Harsh: Second.new 5 calls the associated function — the path uses a
dot, and the argument follows the name.
2.4 The Default Trait
Original: The Default Trait
Default gives a type a value "when nothing else was said". Derived, it fills
every field with its own default; with struct-update syntax, a caller names
only the fields that differ. Containers and generic code rely on it
(unwrap_or_default, mem::take).
#[derive Debug Default]
struct Config
name: String
verbose: bool
retries: u8
tags: Vec<String>
fn main$:
// Every field its own default.
let plain = Config.default$
println! "{:?}" plain
// Only what differs, the rest from the default.
let loud =
Config\
verbose = true
retries = 3
..Config.default$
println! "{:?}" loud
// Containers use it too.
let missing: Option<Vec<i32>> = None
println! "{:?}" (missing <- unwrap_or_default$)
Config { name: "", verbose: false, retries: 0, tags: [] }
Config { name: "", verbose: true, retries: 3, tags: [] }
[]
In Harsh: struct update is written inside the literal, Config\ verbose =
true, ..Config.default$ — $ calls a function that takes nothing.
2.5 Collections Are Smart Pointers
Original: Collections Are Smart Pointers
A collection that owns its data should implement Deref to the borrowed view
of it — Vec<T> to [T], String to str. Every method of the borrowed view
is then available on the owner, and a function written for the view accepts
the owner too.
// Written for the borrowed view: a slice.
fn total (xs: &[i32]) -> i32:
xs <- iter$ <- sum$
fn main$:
let scores = vec! 3 1 4 1 5
// A `&Vec<i32>` is accepted where `&[i32]` is asked for.
println! "{}" (total (&scores))
// The slice's methods, reached through the Vec.
println! "{:?} {}" (scores <- first$) (scores <- contains (&4))
14
Some(3) true
In Harsh: the same. Method calls reach through Deref exactly as in Rust:
scores <- first$ is the slice's first, found through the Vec.
2.6 Finalisation in Destructors
Original: Finalisation in Destructors
Rust has no finally. Code that must run however a function ends — returning
early, with ?, or panicking — goes into the Drop of a value created at the
start: when the function ends, the value goes out of scope, and its destructor
runs.
struct Cleanup
what: &'static str
impl Drop for Cleanup
fn drop (&mut self):
println! "cleaning up after {}" (self <- what)
fn work (fail: bool) -> Result<(), String>:
let _guard = Cleanup\ what = "work"
if fail:
// Returning early: the guard still runs.
return Err (String.from "failed")
println! "working"
Ok ()
fn main$:
println! "{:?}" (work false)
println! "{:?}" (work true)
working
cleaning up after work
Ok(())
cleaning up after work
Err("failed")
In Harsh: impl Drop for Cleanup with fn drop (&mut self): beneath —
a declaration takes no :, its method does.
2.7 mem::{take(), replace()}
Original: mem::{take(), replace()}
To change an enum from one variant to another while keeping an owned field —
without cloning it — take the field out with std.mem.take (which leaves the
type's default in its place) or std.mem.replace (which leaves what you give
it), then build the new variant from it.
#[derive Debug]
enum State
Draft
text: String
Published
text: String
views: u32
// From one variant to the other, keeping the text: moved out, not cloned.
fn publish (s: &mut State):
if let State.Draft\ text = s:
let text = std.mem.take text
*s = State.Published\ text = text, views = 0
fn main$:
let mut post = State.Draft\ text = String.from "Hello"
publish (&mut post)
println! "{:?}" post
Published { text: "Hello", views: 0 }
In Harsh: the match opens with \ and its arms bind fields by name,
State.Draft\ text => …, the same mark as the literal.
2.8 On-Stack Dynamic Dispatch
Original: On-Stack Dynamic Dispatch
To choose at run time between values of different types behind one trait,
there is no need to Box them: declare a variable for each type, initialise
only the one the condition picks — deferred conditional initialisation —
and take a &dyn Trait to it. The value lives on the stack; only the reference
is dynamic.
use std.fmt.Display
fn main$:
for big in [true, false]:
// Declared, not initialised: only the branch taken gives one a value.
let number: i32
let text: String
let shown: &dyn Display = if big:
number = 42
&number
else:
text = String.from "forty-two"
&text
println! "{shown}"
42
forty-two
In Harsh: each branch of the if initialises its own variable and ends
with the reference to it; the variables are declared, without a value, before
the if.
2.9 Foreign function interface (FFI)
Original: Foreign function interface (FFI)
Three idioms for code on either side of a C boundary.
2.9.1 Idiomatic Errors
Original: Idiomatic Errors
C has no Result: errors cross the boundary as integers. A flat enum
becomes its #[repr(C)] discriminant; a structured enum, whose variants
carry data, is mapped to one code per variant; a custom error type can give
C a code and, separately, a message as a C string the caller frees. Never let
a panic cross the boundary.
// A flat enum: its discriminant is the code.
#[repr C]
#[derive Clone Copy Debug]
pub enum DatabaseError
IsReadOnly = 1
IoError = 2
FileCorrupted = 3
// A structured enum: one code per variant, the data left behind.
pub enum ParseError
Empty
BadChar char
TooLong usize
impl ParseError
pub fn code (&self) -> i32:
match self\
ParseError.Empty => 1
ParseError.BadChar _ => 2
ParseError.TooLong _ => 3
// For C, only the codes cross.
pub extern "C" fn parse_status (input: i32) -> i32:
let err = match input\
0 => ParseError.Empty
1 => ParseError.BadChar '?'
_ => ParseError.TooLong 99
err <- code$
fn main$:
println! "{}" (DatabaseError.FileCorrupted as i32)
println! "{} {} {}" (parse_status 0) (parse_status 1) (parse_status 2)
3
1 2 3
In Harsh: #[repr C] takes its argument by juxtaposition; the mapping from
a structured enum to codes is a match opening with \.
2.9.2 Accepting Strings
Original: Accepting Strings
A string that comes from C stays C's: borrow it as &CStr with
CStr::from_ptr, for no longer than the call, and keep the unsafe to that one
line — check the pointer, borrow, then work in safe code.
use std.ffi.CStr
use std.os.raw.c_char
// A string from C, borrowed for the call: the unsafe is one line.
pub extern "C" fn log_message (text: *const c_char) -> i32:
if text <- is_null$:
return -1
let message = unsafe: CStr.from_ptr text
// From here on, safe code: a lossy conversion copes with bad UTF-8.
println! "log: {}" (message <- to_string_lossy$)
0
fn main$:
// As C would call it.
let owned = std.ffi.CString.new "hello from C" <- unwrap$
println! "{}" (log_message (owned <- as_ptr$))
println! "{}" (log_message (std.ptr.null$))
log: hello from C
0
-1
In Harsh: the one unsafe line is unsafe: CStr.from_ptr text; everything
after it is ordinary Harsh.
2.9.3 Passing Strings
Original: Passing Strings
To hand a string to C: make the owned CString live as long as C may use the
pointer — bind it, never pass CString::new(..).as_ptr() from a temporary;
keep the unsafe code to the call; if C writes into the buffer, pass a
Vec<u8> instead; and, unless the API says otherwise, keep ownership on the
Rust side.
use std.ffi.CString
use std.os.raw.c_char
extern "C"
fn strlen (s: *const c_char) -> usize
fn main$:
// Bound: it lives to the end of the block, as long as C may read it.
let owned = CString.new "passed to C" <- unwrap$
let n = unsafe: strlen (owned <- as_ptr$)
println! "C counted {n} bytes"
C counted 11 bytes
In Harsh: the CString is bound by a let, so its lifetime is the
block's, visible in the indentation.
2.10 Iterating over an Option
Original: Iterating over an Option
Option is a collection of zero or one element: it implements
IntoIterator. It can extend a collection, be chained onto an iterator, or be
looped over — no if let needed.
fn main$:
let mut names = vec! "Ada" "Grace"
let extra: Option<&str> = Some "Barbara"
// An Option extends a collection like any iterator.
names <- extend extra
println! "{:?}" names
// It chains onto one.
let all: Vec<&str> =
names <- iter$
<- copied$
<- chain (Some "Linus")
<- collect$
println! "{:?}" all
// And it loops: zero times or once.
for name in extra:
println! "hello {name}"
["Ada", "Grace", "Barbara"]
["Ada", "Grace", "Barbara", "Linus"]
hello Barbara
In Harsh: for name in extra: loops over an Option as over anything;
chains read top to bottom with <-.
2.11 Pass Variables to Closure
Original: Pass Variables to Closure
A move closure takes ownership of everything it uses. To decide variable by
variable — clone this one, borrow that one — prepare them in a block just
before the closure, and let the closure be the block's value.
use std.rc.Rc
fn main$:
let num1 = Rc.new 1
let num2 = Rc.new 2
let num3 = Rc.new 3
// Prepared next to the closure: num2 cloned, num3 borrowed.
let closure = do:
let num2 = num2 <- clone$
let num3 = num3 <- as_ref$
move ||: *num1 + *num2 + *num3
println! "{}" (closure$)
// num2 is still ours.
println! "{num2}"
6
2
In Harsh: the block is do: with its lines beneath, and its last line is
its value: the preparation and the closure read as one unit.
2.12 Privacy For Extensibility
Original: Privacy For Extensibility
A public struct with all-public fields can never gain a field without
breaking everyone who builds it with a literal. Mark it #[non_exhaustive]
(or give it one private field) and provide a constructor: fields can then be
added in a later version.
mod shapes
// Fields may be added later without breaking anyone: no literal outside.
#[non_exhaustive]
#[derive Debug]
pub struct Circle
pub radius: f64
impl Circle
pub fn new (radius: f64) -> Self:
Self\ radius = radius
fn main$:
let c = shapes.Circle.new 2.0
println! "{:?} r = {}" c (c <- radius)
Circle { radius: 2.0 } r = 2
In Harsh: mod shapes holds its items beneath, as every declaration does;
the attribute is #[non_exhaustive] above the struct.
2.13 Easy doc initialization
Original: Easy doc initialization
When every documentation example would need the same setup, wrap the example in a function that takes the prepared value as a parameter: the documentation test compiles the function but never calls it, so the setup is never written, and the example shows only what it documents. A helper that builds the value is the alternative when the example must run.
pub struct Connection
pub name: String
impl Connection
/// Sends a message.
///
/// The example is a function taking the connection: compiled, never
/// called, so no connection has to be set up to show `send`.
///
/// ```
/// fn call_send (c: Connection):
/// c <- send "hi"
/// ```
pub fn send (&self) (msg: &str):
println! "{} <- {msg}" (self <- name)
fn main$:
let c = Connection\ name = String.from "test"
c <- send "hi"
test <- hi
In Harsh: documentation examples in /// comments are written in Harsh,
and hrs test transpiles and runs them — with the same fences as Rust's.
2.14 Temporary mutability
Original: Temporary mutability
When data must be prepared mutably and then only read, shadow it: build it in a nested block (or a mutable binding) and rebind it immutable. The compiler then refuses any later change.
fn main$:
// Prepared mutably in a block, then bound immutable.
let data = do:
let mut d = vec! 5 3 9 1
d <- sort$
d
println! "{:?}" data
// Or shadowed: the same name, no longer `mut`.
let mut total = 0
for x in &data:
total += x
let total = total
println! "{total}"
[1, 3, 5, 9]
18
In Harsh: the nested block is do:; its value is its last line, and the
let that receives it is immutable.
2.15 Return consumed arg on error
Original: Return consumed arg on error
A function that takes ownership of an argument and may fail should hand the
argument back in its error, so the caller can try again without having cloned
it first — as String::from_utf8 returns the bytes in its FromUtf8Error.
// The argument, handed back in the error.
#[derive Debug]
pub struct SendError String
pub fn send (value: String) (attempt: u32) -> Result<(), SendError>:
if attempt < 2:
return Err (SendError value)
println! "sent {value:?} on attempt {attempt}"
Ok ()
fn main$:
let mut value = String.from "important"
for attempt in 0..3:
match send value attempt\
Ok () => break
Err (SendError v) =>
println! "attempt {attempt} failed; retrying"
value = v
attempt 0 failed; retrying
attempt 1 failed; retrying
sent "important" on attempt 2
In Harsh: the error is a tuple struct, SendError String, taken apart by
juxtaposition: Err (SendError v) => ….
Behavioural patterns
Patterns about how values cooperate: who decides what happens, and when. In Rust several of them shrink, because closures and traits already carry behaviour; in Harsh they shrink further, since a closure is often a partial application and a list of steps reads as one. The original chapter: Behavioural patterns.
3.1 Command
Original: Command
Turn actions into values, so they can be stored, queued, logged, or undone. In Rust there are three ways: trait objects (one type per command), function pointers, or closures — the last the most flexible when commands carry data.
// Commands as values: a trait object each.
trait Migration
fn execute (&self) -> String
fn rollback (&self) -> String
struct CreateTable
struct AddField
impl Migration for CreateTable
fn execute (&self) -> String:
String.from "create table"
fn rollback (&self) -> String:
String.from "drop table"
impl Migration for AddField
fn execute (&self) -> String:
String.from "add field"
fn rollback (&self) -> String:
String.from "remove field"
fn create (table: &str) -> String:
format! "create {table}"
fn main$:
let steps: Vec<Box<dyn Migration>> =
vec! (Box.new CreateTable) (Box.new AddField)
let done: Vec<String> =
steps <- iter$
<- map (|m| m <- execute$)
<- collect$
println! "{:?}" done
let undone: Vec<String> =
steps <- iter$
<- rev$
<- map (|m| m <- rollback$)
<- collect$
println! "{:?}" undone
// A command as a function given its arguments, waiting to run.
let later = || create "users"
println! "{}" (later$)
["create table", "add field"]
["remove field", "drop table"]
create users
In Harsh: with partial application a command is often just a function
given some of its arguments: "users" |> create is create waiting for
nothing more than the moment to run.
3.2 Interpreter
Original: Interpreter
For a problem that recurs in many forms, define a small language for it and interpret sentences of that language — here, arithmetic expressions turned from infix into postfix by a recursive-descent parser.
// Infix to postfix: `2+3-4` becomes `23+4-`.
struct Interpreter<'a>
it: std.str.Chars<'a>
impl<'a> Interpreter<'a>
fn new (infix: &'a str) -> Self:
Self\ it = infix <- chars$
fn next_char (&mut self) -> Option<char>:
self <- it <- next$
// exp -> term ( ('+' | '-') term )*
fn interpret (&mut self) (out: &mut String):
self <- term out
while let Some op = self <- next_char$:
if op == '+' || op == '-':
self <- term out
out <- push op
else:
panic! "unexpected symbol '{op}'"
// term -> a single digit
fn term (&mut self) (out: &mut String):
match self <- next_char$\
Some d if d <- is_ascii_digit$ => out <- push d
Some c => panic! "unexpected symbol '{c}'"
None => panic! "unexpected end of input"
fn main$:
let mut postfix = String.new$
Interpreter.new "2+3" <- interpret (&mut postfix)
println! "{postfix}"
postfix <- clear$
Interpreter.new "1-2+3-4" <- interpret (&mut postfix)
println! "{postfix}"
23+
12-3+4-
In Harsh: the parser's recursion reads as its grammar: each rule a short
function, each alternative a match arm.
3.3 Newtype
Original: Newtype
Wrap a type in a tuple struct of one field to give it a new identity: its own traits, its own rules, and no mixing with the type it wraps. It costs nothing at run time.
use std.fmt
// A String with its own identity: never printed in clear.
struct Password String
impl fmt.Display for Password
fn fmt (&self) (f: &mut fmt.Formatter) -> fmt.Result:
write! f "{}" ("*" <- repeat (self.0 <- len$))
fn main$:
let unsecured = String.from "ThisIsMyPassword"
let secured = Password (unsecured <- clone$)
println! "unsecured: {unsecured}"
println! "secured: {secured}"
unsecured: ThisIsMyPassword
secured: ****************
In Harsh: a tuple struct's field juxtaposes, struct Password String, and
is read with .0, as self.0.
3.4 RAII Guards
Original: RAII Guards
Tie a resource to a value's lifetime: acquiring it returns a guard, and
dropping the guard releases it. The borrow checker then guarantees nothing
uses the resource after release — MutexGuard is the standard example.
use std.sync.Mutex
struct Noisy
name: &'static str
impl Drop for Noisy
fn drop (&mut self):
println! "released {}" (self <- name)
fn main$:
let counter = Mutex.new 0
// The guard's scope is the block: the lock is held just this long.
if true:
let mut guard = counter <- lock$ <- unwrap$
*guard += 1
let _n = Noisy\ name = "a guard of our own"
println! "inside: {}" (*guard)
println! "after: {}" (counter <- lock$ <- unwrap$)
inside: 1
released a guard of our own
after: 1
In Harsh: a guard's scope is its block; the indentation shows exactly how long the lock is held.
3.5 Strategy
Original: Strategy
Separate an algorithm's skeleton from its details, so the details can vary: the skeleton takes a strategy, as a trait object, a generic, or a closure.
// The skeleton: a report, with the joining left to a strategy.
// The lifetime is named: a closure made by partial application works for
// these items, not for every lifetime at once.
fn report<'a> (items: &'a [&'a str]) (join: impl Fn (&'a [&'a str]) -> String) -> String:
format! "report: {}" (join items)
fn join_with (separator: &str) (items: &[&str]) -> String:
items <- join separator
fn main$:
let items = ["alpha", "beta", "gamma"]
// A strategy from an ordinary function, by partial application.
let commas = ", " |> join_with
let lines = "\n " |> join_with
println! "{}" (report (&items) commas)
println! "{}" (report (&items) lines)
// Or a closure.
println! "{}" (report (&items) (|xs| xs <- len$ <- to_string$))
report: alpha, beta, gamma
report: alpha
beta
gamma
report: 3
In Harsh: partial application makes strategies out of ordinary functions:
", " |> join_with is a strategy that joins with commas, with no closure
written by hand. One Rust subtlety shows here: such a closure works for the
items it is given, not for every lifetime at once, so the skeleton names its
lifetime, report<'a>.
3.6 Visitor
Original: Visitor
Walk a heterogeneous structure — an abstract syntax tree, say — with an operation that is defined apart from it. The visitor has a method per kind of node; the structure only knows how to hand each node to it.
// A tiny language: numbers and additions.
enum Expr
Num i64
Add (Box<Expr>) (Box<Expr>)
trait Visitor<T>
fn visit (&mut self) (e: &Expr) -> T
// One operation: evaluate.
struct Eval
impl Visitor<i64> for Eval
fn visit (&mut self) (e: &Expr) -> i64:
match e\
Expr.Num n => *n
Expr.Add a b => self <- visit a + self <- visit b
// Another, defined apart from the tree: print.
struct Show
impl Visitor<String> for Show
fn visit (&mut self) (e: &Expr) -> String:
match e\
Expr.Num n => n <- to_string$
Expr.Add a b =>
format! "({} + {})" (self <- visit a) (self <- visit b)
fn main$:
let inner = Expr.Add (Box.new (Expr.Num 2)) (Box.new (Expr.Num 3))
let e = Expr.Add (Box.new (Expr.Num 1)) (Box.new inner)
println! "{} = {}" (Show <- visit (&e)) (Eval <- visit (&e))
(1 + (2 + 3)) = 6
In Harsh: the trait's methods and the walk read the same as in Rust; the
match over node kinds opens with \.
Creational patterns
Patterns for building values. The original chapter: Creational patterns.
4.1 Builder
Original: Builder
When a value has many optional parts, build it step by step through a
builder: each method sets one part and returns the builder, and build
makes the value. Rust has no named or default arguments; this is its answer.
#[derive Debug]
pub struct Request
url: String
method: String
headers: Vec<(String, String)>
body: Option<String>
pub struct RequestBuilder
request: Request
impl RequestBuilder
pub fn new (url: &str) -> Self:
let request =
Request\
url = url <- to_string$
method = String.from "GET"
headers = Vec.new$
body = None
Self\ request = request
pub fn method (mut self) (m: &str) -> Self:
self <- request <- method = m <- to_string$
self
pub fn header (mut self) (k: &str) (v: &str) -> Self:
self <- request
<- headers
<- push (k <- to_string$, v <- to_string$)
self
pub fn body (mut self) (b: &str) -> Self:
self <- request <- body = Some (b <- to_string$)
self
pub fn build (self) -> Request:
self <- request
fn main$:
// One line per choice: the layout of a builder is its list of options.
let req =
RequestBuilder.new "https://harsh-lang.com"
<- method "POST"
<- header "Accept" "text/html"
<- body "hello"
<- build$
println! "{:#?}" req
Request {
url: "https://harsh-lang.com",
method: "POST",
headers: [
(
"Accept",
"text/html",
),
],
body: Some(
"hello",
),
}
In Harsh: a chain is one line per step, <- at the start of each — the
layout of a builder in Harsh is the list of its choices.
4.2 Fold
Original: Fold
Run a transformation over every node of a structure, producing a new structure: a folder has a method per kind of node, each returning the rebuilt node — the default rebuilding it unchanged, so a folder overrides only what it changes.
// A small tree of names and calls.
#[derive Debug]
enum Node
Name String
Call String (Vec<Node>)
// A folder: a method per kind of node, each returning the rebuilt node.
trait Folder
fn fold_name (&mut self) (n: String) -> Node:
Node.Name n
fn fold_call (&mut self) (f: String) (args: Vec<Node>) -> Node:
let args =
args <- into_iter$
<- map (|a| self <- fold a)
<- collect$
Node.Call f args
fn fold (&mut self) (n: Node) -> Node:
match n\
Node.Name s => self <- fold_name s
Node.Call f args => self <- fold_call f args
// This folder overrides one method: it renames every name.
struct Renamer
impl Folder for Renamer
fn fold_name (&mut self) (n: String) -> Node:
Node.Name (format! "{n}_renamed")
fn main$:
let x = Node.Name (String.from "x")
let y = Node.Name (String.from "y")
let tree = Node.Call (String.from "f") (vec! x y)
println! "{:?}" (Renamer <- fold tree)
Call("f", [Name("x_renamed"), Name("y_renamed")])
In Harsh: the default methods sit in the trait, and a folder that renames identifiers overrides one of them.
Structural patterns
Patterns for arranging types and modules. The original chapter: Structural patterns.
5.1 Compose Structs
Original: Compose Structs
A large struct borrowed as a whole cannot lend two of its parts at once to different functions. Split it into smaller structs, and each function borrows only the part it needs — the borrow checker then sees they do not overlap.
struct Connection
url: String
struct Users
names: Vec<String>
// Two parts instead of one large struct: each can be borrowed alone.
struct Database
connection: Connection
users: Users
fn log_connection (c: &Connection):
println! "connected to {}" (c <- url)
fn add_user (u: &mut Users) (name: &str):
u <- names <- push (name <- to_string$)
fn main$:
let connection = Connection\ url = String.from "db://local"
let users = Users\ names = Vec.new$
let mut db = Database\ connection = connection, users = users
// A shared borrow of one part, a mutable one of the other: no conflict.
let c = &db <- connection
add_user (&mut db <- users) "ada"
log_connection c
println! "{:?}" (db <- users <- names)
connected to db://local
["ada"]
In Harsh: field access is <-, so db <- connection and db <- users
are the two independent borrows.
5.2 Prefer Small Crates
Original: Prefer Small Crates
Small crates that do one thing are easier to understand, reuse and compile
in parallel; the cost is more dependencies to manage. Harsh works with the
same crates as Rust — every crate on crates.io is a Harsh dependency — and a
Harsh library of your own is shared as Rust with hrs export, or as Harsh by
path (The Harsh Programming Language, chapter 17).
There is no program on this page: the pattern is about how a project is cut, not about code.
5.3 Contain unsafety in small modules
Original: Contain unsafety in small modules
Keep unsafe code in the smallest module that can hold it, with a safe
interface around it: the module's invariants are then checked in one place,
and the rest of the program cannot break them.
mod ascii
// The invariant, kept in this module alone: the bytes are ASCII.
pub struct AsciiString
bytes: Vec<u8>
impl AsciiString
pub fn new (s: &str) -> Option<Self>:
if s <- is_ascii$:
Some (Self\ bytes = s <- as_bytes$ <- to_vec$)
else:
None
pub fn as_str (&self) -> &str:
// Safe, because `new` checked the invariant.
unsafe: std.str.from_utf8_unchecked (&self <- bytes)
fn main$:
let a = ascii.AsciiString.new "hello" <- unwrap$
println! "{}" (a <- as_str$)
println! "{}" (ascii.AsciiString.new "héllo" <- is_none$)
hello
true
In Harsh: unsafe: opens a block like any keyword; the module's safe
functions around it are ordinary Harsh.
5.4 Avoid complex type bounds with custom traits
Original: Avoid complex type bounds with custom traits
A struct generic over closures carries their whole signatures as bounds —
G: FnMut() -> Result<T, Error>, and a T just to name the output — and every
impl repeats them. Name the shape once: a trait with an associated type
for the output, implemented for every closure of that shape. The struct then
asks for G: Getter, and T disappears from its parameters.
use std.fmt.Display
#[derive Debug]
pub struct Error
#[derive Debug]
pub enum Status
Fresh
Stale
// The closure's shape, named once: its output is an associated type.
pub trait Getter
type Output: Display
fn get_value (&mut self) -> Result<Self.Output, Error>
// Every closure of that shape is a Getter.
impl<F: FnMut$ -> Result<T, Error>, T: Display> Getter for F
type Output = T
fn get_value (&mut self) -> Result<T, Error>:
self$
// No `T`, and no closure signature, in the struct's parameters.
pub struct Value<G: Getter, S: Fn (&G.Output) -> Status>
value: Option<G.Output>
getter: G
status: S
impl<G: Getter, S: Fn (&G.Output) -> Status> Value<G, S>
pub fn update (&mut self) -> Result<(), Error>:
self <- value = Some ((self <- getter <- get_value$)?)
Ok ()
pub fn status (&self) -> Option<Status>:
self <- value
<- as_ref$
<- map (&self <- status)
fn main$:
let mut n = 0
let getter = move ||:
n += 1
let r: Result<i32, Error> = Ok n
r
let mut v =
Value\
value = None
getter = getter
status = |x: &i32| if *x > 1: Status.Stale else: Status.Fresh
for _ in 0..2:
v <- update$ <- unwrap$
println! "{:?} {:?}" (v <- value) (v <- status$)
Some(1) Some(Fresh)
Some(2) Some(Stale)
A related variant, for a list of bounds rather than a closure's shape: a trait with those bounds as supertraits, and a blanket implementation for every type that meets them.
use std.fmt.( Debug, Display)
// The long list of bounds, named once.
trait Loggable: Debug + Display + Clone + PartialEq
impl<T: Debug + Display + Clone + PartialEq> Loggable for T
// Each function asks for the one trait.
fn log_twice<T: Loggable> (x: &T):
let copy = T.clone x
println! "{x} / {copy:?} / same: {}" (*x == copy)
fn main$:
log_twice (&42)
log_twice (&String.from "hello")
42 / 42 / same: true
hello / "hello" / same: true
In Harsh: Self.Output and G.Output are the associated type's paths,
with dots. In the variant, the trait and its blanket impl are one line each,
with no body —
trait Loggable: Debug + Display + … and impl<T: …> Loggable for T —
and come out as Rust's {} bodies. (Writing this page found that such
empty-bodied headers were misread, a : in them taken for a block; fixed in
Harsh 0.1.44.)
Foreign function interface
Patterns for code that crosses into C and back. The original chapter: FFI patterns.
6.1 Object-Based APIs
Original: Object-Based APIs
Expose a Rust type to C as an opaque handle: C gets a pointer it cannot look
inside, and every operation is a function taking that pointer. Ownership is
explicit — one function creates the object (Box::into_raw), one destroys it
(Box::from_raw).
// An opaque object for C: C holds the pointer, never the inside.
pub struct Counter
count: u64
pub extern "C" fn counter_new$ -> *mut Counter:
Box.into_raw (Box.new (Counter\ count = 0))
pub extern "C" fn counter_add (c: *mut Counter) (n: u64):
let c = unsafe: &mut *c
c <- count += n
pub extern "C" fn counter_get (c: *const Counter) -> u64:
unsafe: (*c) <- count
pub extern "C" fn counter_free (c: *mut Counter):
if !c <- is_null$:
drop (unsafe: Box.from_raw c)
fn main$:
// As C would use it.
let c = counter_new$
counter_add c 5
counter_add c 7
println! "{}" (counter_get c)
counter_free c
12
In Harsh: the exported functions are pub extern "C" fn, and each
dereference of the handle sits in its own unsafe: block.
6.2 Type Consolidation into Wrappers
Original: Type Consolidation into Wrappers
Rust types with lifetimes do not cross into C. Wrap the owner and the state that borrows from it into one owned type — here, a collection and a cursor over it — and expose that single type instead.
// For C: one owned type, where Rust would have a collection and an iterator
// borrowing from it -- a lifetime C cannot express.
pub struct Words
words: Vec<String>
next: usize
impl Words
pub fn new (text: &str) -> Self:
let words =
text <- split_whitespace$
<- map String.from
<- collect$
Self\ words = words, next = 0
pub fn next (&mut self) -> Option<&str>:
let w = self <- words <- get (self <- next)?
self <- next += 1
Some (w <- as_str$)
fn main$:
let mut w = Words.new "one two three"
while let Some word = w <- next$:
println! "{word}"
one
two
three
In Harsh: the wrapper is a plain struct with a next method; the
lifetime that C could not express is gone from its interface.
Anti-patterns
Arrangements that look like solutions and create more problems than they solve. Knowing them is half of avoiding them. The original chapter: Anti-patterns.
7.1 Clone to satisfy the borrow checker
Original: Clone to satisfy the borrow checker
When the borrow checker refuses code, cloning the value makes the error go away — and often hides the real mistake: two copies now drift apart, and the change made to one is missing from the other. First the refusal:
fn main$:
let mut names = vec! (String.from "Ada")
let first = &names[0]
// Refused: `names` is borrowed by `first` while we push.
names <- push (String.from "Grace")
println! "{first}"
error[E0502]: cannot borrow `names` as mutable because it is also borrowed as immutable
--> clone_refused.hrs:5:5
|
3 | let first = &names[0]
| ----- immutable borrow occurs here
5 | names <- push (String.from "Grace")
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ mutable borrow occurs here
6 | println! "{first}"
| ----- immutable borrow later used here
then the fix that keeps one value, by ending the first borrow before the second begins, instead of cloning:
fn main$:
let mut names = vec! (String.from "Ada")
// The first borrow ends before the second begins: one value, no clone.
println! "{}" (&names[0])
names <- push (String.from "Grace")
println! "{:?}" names
Ada
["Ada", "Grace"]
In Harsh: rustc's error points at the .hrs line it concerns, so the
borrow to shorten is found where you wrote it.
7.2 #[deny(warnings)]
Original: #[deny(warnings)]
Denying all warnings in the source makes a build fail on the next compiler
that adds a warning — breaking code that did nothing wrong, and every crate
that depends on it. Deny specific lints you care about, or deny warnings in
continuous integration only (RUSTFLAGS="-D warnings"), never in the crate.
// Deny the specific lints you mean, not every warning a future compiler adds.
#![deny unused_must_use]
fn checked (n: i32) -> Result<i32, String>:
if n >= 0: Ok n else: Err (String.from "negative")
fn main$:
// With `unused_must_use` denied, ignoring this Result would not compile.
let r = checked 5
println! "{:?}" r
Ok(5)
In Harsh: attributes take their arguments by juxtaposition, #![deny
unused_must_use]; and hrs check passes cargo's flags through, so CI's
-D warnings works as for Rust.
7.3 Deref Polymorphism
Original: Deref Polymorphism
Implementing Deref to make one struct "inherit" another's methods imitates
inheritance, and misleads: Deref is for smart pointers, the target's traits
are not inherited, and generic code does not see through it. Compose instead,
and forward the few methods that should be shared.
struct Animal
name: String
impl Animal
fn name (&self) -> &str:
&self <- name
// Composition, not `Deref` to Animal: the shared method is forwarded.
struct Dog
inner: Animal
good: bool
impl Dog
fn name (&self) -> &str:
self <- inner <- name$
fn describe (&self) -> String:
format!
"{} ({})"
(self <- name$)
(if self <- good: "good dog" else: "dog")
fn main$:
let d =
Dog\
inner = Animal\ name = String.from "Rex"
good = true
println! "{}" (d <- describe$)
Rex (good dog)
In Harsh: the forwarding method is one line, self <- inner <- name$.
Functional Programming
Rust borrows much from functional languages — closures, iterators, algebraic types — and Harsh leans further that way: pipes, partial application and comprehensions are its own. This chapter is where the two books differ most. The original chapter: Functional usage of Rust.
8.1 Programming paradigms
Original: Programming paradigms
Imperative code says how: a counter, a loop, a mutation per step. Declarative code says what: the result as a composition of operations. Rust supports both; iterators make the declarative form idiomatic. Harsh adds two more declarative forms — the pipe and the comprehension.
fn square (n: u32) -> u32:
n * n
fn report (total: u32) -> String:
format! "the sum of squares is {total}"
fn main$:
// Imperative: how.
let mut total = 0
for n in 1..=10:
total += n * n
println! "{total}"
// Declarative: what -- an iterator chain.
let total: u32 = (1..=10) <- map square <- sum$
println! "{total}"
// A pipe: the result flows into the next function.
let line = ((1..=10) <- map square <- sum$) |> report
println! "{line}"
// A comprehension.
let squares = list~ n * n for n in 1..=10u32
let total: u32 = squares <- iter$ <- sum$
println! "{total}"
385
385
the sum of squares is 385
385
In Harsh: the same sum four ways — a loop, an iterator chain, a pipe through a function, and a comprehension. The last three say what is computed; only the first says how.
8.2 Generics as Type Classes
Original: Generics as Type Classes
A generic type's parameters can select which methods exist: an impl for one
instantiation gives methods to that instantiation only — the way type classes
work in functional languages. The compiler then refuses, at compile time, a
method on the wrong kind of value.
use std.marker.PhantomData
struct Http
struct Ftp
// One request type; its parameter selects what it can do.
struct Request<P>
url: String
protocol: PhantomData<P>
impl<P> Request<P>
fn url (&self) -> &str:
&self <- url
impl Request<Http>
fn new_http (url: &str) -> Self:
Self\ url = url <- to_string$, protocol = PhantomData
fn post (&self) (body: &str) -> String:
format! "POST {} with {body:?}" (self <- url$)
impl Request<Ftp>
fn new_ftp (url: &str) -> Self:
Self\ url = url <- to_string$, protocol = PhantomData
fn list (&self) -> String:
format! "LIST {}" (self <- url$)
fn main$:
let h = Request.new_http "http://harsh-lang.com"
let f = Request.new_ftp "ftp://example.com"
println! "{}" (h <- post "hi")
println! "{}" (f <- list$)
// `h <- list$` would not compile: `list` exists for FTP requests only.
POST http://harsh-lang.com with "hi"
LIST ftp://example.com
In Harsh: the two impl blocks, for Request<Http> and Request<Ftp>,
read as two short lists of what each protocol allows.
8.3 Functional Optics
Original: Functional Optics
Optics are the functional vocabulary for conversions between types. An iso converts both ways and loses nothing; a poly iso is an iso that works for every type parameter; a prism converts one way always and the other way only sometimes — a parse that may fail, and the rendering that always succeeds. The original uses them to explain the design of Serde's API, whose deserializer drives a visitor that may fail — a prism in all but name.
use std.collections.VecDeque
// An iso: two conversions that undo each other.
#[derive Debug Clone Copy PartialEq]
struct Celsius f64
#[derive Debug Clone Copy PartialEq]
struct Fahrenheit f64
fn to_f (c: Celsius) -> Fahrenheit:
Fahrenheit (c.0 * 9.0 / 5.0 + 32.0)
fn to_c (f: Fahrenheit) -> Celsius:
Celsius ((f.0 - 32.0) * 5.0 / 9.0)
// A poly iso: the same, for every element type.
fn to_deque<T> (v: Vec<T>) -> VecDeque<T>:
VecDeque.from v
fn to_vec<T> (d: VecDeque<T>) -> Vec<T>:
Vec.from d
// A prism: one way may fail, the other always succeeds.
fn preview (s: &str) -> Option<u16>:
s <- parse$ <- ok$
fn review (port: u16) -> String:
port <- to_string$
fn main$:
let boiling = Celsius 100.0
println!
"{:?} and back: {}"
(to_f boiling)
(to_c (to_f boiling) == boiling)
println! "{:?}" (to_vec (to_deque (vec! 'a' 'b')))
println! "{:?} {:?}" (preview "8080") (preview "http")
println! "{}" (preview (&review 443) == Some 443)
Fahrenheit(212.0) and back: true
['a', 'b']
Some(8080) None
true
In Harsh: each optic is a pair of plain functions; the round trips at the end are the laws they obey.
Additional Resources
Original: Additional Resources
The original closes with material behind the patterns rather than patterns themselves; its main page is the design principles.
9.1 Design principles
Original: Design principles
Principles older than Rust, which hold in Harsh unchanged; this companion names them and leaves their discussion to the original:
- SOLID — a unit has one responsibility; open to extension, closed to modification; subtypes stand in for their types; small, specific interfaces; depend on abstractions (in Rust: traits), not concrete types.
- Composite reuse, or composition over inheritance — which Rust enforces, having no inheritance; see Deref Polymorphism.
- DRY — every piece of knowledge has one representation in the system.
- KISS — most systems work best kept simple.
- Law of Demeter — a unit talks to its immediate collaborators only.
- Design by contract — preconditions, postconditions and invariants, stated and checked.
- Encapsulation — the invariants a module keeps are its own business; see Contain unsafety in small modules.
- Command–query separation — a function either changes something or answers a question, not both.
- Principle of least astonishment — a component behaves as its users expect.
- Linguistic modular units — modules are units of the language itself, as
Rust's
modand crates are. - Self-documentation — the documentation lives with the code it
describes, as
///comments and their tested examples do. - Uniform access — a service is used the same way whether it is stored or computed.
- Single choice — where a system must choose among alternatives, one
module alone knows the full list — as one
enumand itsmatches do. - Persistence closure — storing a value stores everything it depends on.
Harsh's own principle is the one Rust keeps: say what you mean, and let the compiler check it. The layout and the pipe only make what you mean easier to read.