Renyi Cheat Sheet

Renyi is a statically typed language whose syntax is regular English. Every concept has one spelling. Blocks end with end. Indentation is never meaning. There is no =, no ==, no null, no exceptions, no anonymous functions.

Module

module billing.invoices
  purpose: Compute customer invoices.

import std.json                              # use as json.parse(...)
import std.http as web                       # renamed namespace
import accounts.models exposing User, UserId # types and abilities only

Imported functions are called qualified, own-module ones bare. Definitions are private unless public.

Function

public function total_price(items: List of Item, rate: TaxRate)
  returns Money
  or fails with PricingError
  needs network.http
  for any Item where Item can Priced
  purpose: Add up every item and apply the tax rate.
  tags: billing, money
  see also: tax_for
  example: total_price(items: [Item(price: 10.00)], rate: TaxRate(0.1)) is 11.00

  let subtotal be for each item in items sum item.price
  return subtotal + tax.tax_for(amount: subtotal, rate: rate)
end

A function whose first parameter is self: Text is a method: text.trim(). Clause order: returns, or fails with, needs, for any, purpose:, tags:, see also:, deprecated:, expose as tool, example: lines, blank line, body. Omit returns when nothing is returned. The module line and every public function, type and constant must carry a purpose: line; the checker rejects one without it.

Calls

let page be web.get(url) otherwise fail
let total be money.add(left: 1, right: 2)          # two or more: all named, in order
retry.run(action: fetch_page, attempts: 3)         # pass a function by name

One argument is never named: web.get(url), not web.get(url: url).

Bindings

let total be 0                       # immutable
let users: List of User be json.parse(text) otherwise fail   # pin a type
let mutable count be 0               # mutable local
change count to count + 1            # the only mutation
ignore connection.execute(sql)       # discard the result of a call with effects

A name is bound once per scope; shadowing is an error. Every binding must be used, and an unused result is an error (change items to items.append(item)), as is ignore of a pure call's result.

Types

public type User
  purpose: A registered account holder.
  has name: Text
  has age: Integer where age is at least 0      # refinement
  has email: maybe Email                        # optional field
  has kind: Text as "type"                      # external name for JSON
  can Compare by name                           # derived ability
  can ToJson
end

public type Shape is one of
  purpose: What the plotter can draw.
  Circle(radius: Decimal)
  Rectangle(width: Decimal, height: Decimal)
  Point
end

public type Email is Text where value.matches(email_pattern)
  purpose: A syntactically valid address.
public type UserId is Integer                   # distinct alias
  purpose: The key of a user row.
public type Pair of Left, Right
  purpose: Two values carried together.
  has left: Left
  has right: Right
end

Construct: User(name: "Ann", age: 30, email: nothing), Circle(radius: 2.5) (fields always named), Point, UserId(7) (a subtype wraps one value). Update: user with age: 31. Refined construction or update can fail: Email(input) otherwise fail with BadInput. A literal the checker can evaluate needs no otherwise (Port(8080)); a variable needs one even after a check. Generic types: List of T, Map of K to V, Set of T, maybe T.

Abilities

ability Describable
  function describe(self) returns Text
end

ability Describable for Shape
  function describe(self) returns Text
    match self
      when Circle(radius) then return "circle of radius {radius}"
      when Point then return "point"
    end
  end
end

Derivable: Equal, Compare by, Hash, ToText (a variant prints as its bare name, Green; a record in constructor form), ToJson, FromJson. Iterable of Item is implemented (to_list(self) returns List of Item) and for each walks the type; collections, ranges and texts have it too, so for any Bag, Item where Bag can Iterable of Item walks any. Generic: ability Sized for Stack of Item then for any Item.

Expressions

a + b   a - b   a * b   a / b   a remainder b   a power b
a is b   a is not b   a is less than b   a is at most b
a is greater than b   a is at least b
a and b   a or b   not a
"Hello {user.name}, total {total}"         # interpolation; a literal brace is \{
raw "^[0-9]{4}$"                           # no holes, no escapes
from 1 to 10   from 0 to 100 by 5         # inclusive ranges
[1, 2, 3]   {"key": value}   nothing   true   false

Numbers: Integer (unbounded), Decimal (decimal128, 19.99), Float. No implicit conversion: count.to_decimal(). is compares values of one type: 32.0 is 32.00. / needs Decimal or Float operands; a.quotient(b) divides two Integers down.

Optionals and errors

let name be user.nickname otherwise user.name       # default
let text be files.read_text(path) otherwise fail    # propagate
let config: Config be json.parse(text) otherwise fail with BadConfig(detail: "not JSON")
let user be accounts.find(id) otherwise return nothing
fail with NotFound(path: path)                      # return a failure
crash with "unreachable: {state}"                   # terminate; bugs only

match action()                                      # when the error is needed
  when success(outcome) then return outcome
  when failure(error) then log.warn(error.to_text())
end

A T is accepted where maybe T or a success is expected. Reading out always names the other case, and otherwise is allowed only there. It takes a default value or a way out: fail, fail with E(...), return value, break, continue, crash with "text". type X is Base is a subtype: an X passes as a Base, never the reverse.

Control flow

if age is at least 18 then
  change adults to adults + 1
otherwise if age is at least 13 then
  change teens to teens + 1
otherwise
  change children to children + 1
end

match shape
  when Circle(radius) where radius is greater than 10 then return Large
  when Circle(radius) then return Small
  when Rectangle(width: wide, height: tall) then return area.classify(width: wide, height: tall)
  otherwise return Unknown
end

match user.nickname
  when some(nickname) then console.print(nickname)
  when nothing then console.print(user.name)
end

for each line in invoice.lines
  change total to total + line.amount
end
for each key, value in settings
  console.print("{key}: {value}")
end
repeat until attempts is at least 3
  change attempts to attempts + 1
  if done then break end
end

Matching is exhaustive. A variant with fields matches by its bare name when no field is needed: when Circle then. if and match are also expressions when every branch is one expression: let label be if done then "yes" otherwise "no" end. Loop headers accept where and sorted by.

Queries (instead of lambdas)

let emails be
  for each user in users
  where user.is_active and user.age is at least 18
  sorted by user.created_at descending
  collect user.email

for each order in orders where order.is_paid sum order.total     # Decimal
for each order in orders where order.is_paid count               # Integer
for each order in orders sorted by order.created_at first        # maybe Order
for each order in orders all order.is_shipped                    # Boolean
for each order in orders any order.is_refunded                   # Boolean
for each user in users group by user.country collect user.email # Map
for each sale in sales group by sale.region sum sale.amount     # Map of Text to Decimal
for each order in orders, line in order.lines collect line.sku
for each url in urls concurrently collect web.get(url) otherwise fail

Effects

needs lists capabilities; callers declare a superset; no needs means pure. Capabilities: console, filesystem.read, filesystem.write, network.http, network.socket, environment, time, random, process, foreign, python. A parent covers its children. A literal argument narrows a scope: filesystem.read("data"), network.http("api.example.com"); no argument covers every scope. main declares the whole grant. Only there: at most 60 per minute (budget; also per run) and only to console (what it reads leaves only there).

public function main() or fails with AppError
  needs console, network.http("api.example.com") at most 60 per minute
  purpose: Print today's report.
  ...
end

Concurrency

run concurrently within time.seconds(5)
  let users be accounts.fetch_all() otherwise fail
  let orders be billing.fetch_open() otherwise fail
end

All tasks finish or the first failure cancels the rest; an expired within deadline cancels them and fails with TimedOut.

Tests and tools

test "tax is applied to the subtotal"
  check invoices.total_price(items: items, rate: TaxRate(0.1)) is 11.00
end
test "the forecast is read" needs network.http replays "fixtures/forecast.json"
  check weather.fetch(city: "Berlin").temperature is 21.5
end

A recording (renyi record) answers a replays test's effects offline. expose as tool publishes a function to agents (schema from the parameters, description from purpose:, permissions from needs; renyi tools prints it). deprecated: since 2.0, replaced by new_name warns callers.

Library (names and parameters; nothing else exists)

Text: length() is_empty() trim() trim_start() trim_end() to_lower() to_upper()
  split(separator) lines() characters() contains(part) starts_with(prefix)
  ends_with(suffix) index_of(part) replace(old, new) pad_left(width)
  pad_right(width) repeat(times) take(length) drop(length) reversed()
  matches(pattern) to_integer() to_decimal() to_float() to_bytes()
List: length() is_empty() at(index) first() last() rest() without_last()
  without_index(index) take(length) drop(length) slice(start, stop) append(item)
  append_all(others) reversed() sorted() distinct() contains(item)
  index_of(item) largest() smallest() with_index() to_set() flattened()
  join(separator) sum()
Map: length() is_empty() get(key) set(key, value) without(key)
  contains_key(key) keys() values() entries() merged(other)
Set: length() is_empty() contains(item) add(item) without(item) union(other)
  intersection(other) difference(other) is_subset_of(other) sorted() to_list()
Numbers: to_decimal() to_float() to_text() quotient(divisor) absolute()
  at_least(other) at_most(other) rounded(places) truncated() square_root()
Modules: std.console (print, print_error, read_line); std.environment
  (arguments, get, exit); std.time (now, seconds, parse_date, Date,
  Instant, Duration); std.random; std.filesystem (Path, read_text, write_text,
  exists, list); std.json (parse, render); std.http (Url, get, post_json);
  std.server; std.csv; std.sqlite; std.regex; std.process (execute, attempt);
  std.foreign; std.python

One parameter is positional, more are named: line.split(","), text.replace(old: "a", new: "b").

Names and reserved words

snake_case for values and functions, PascalCase for types, abilities and variants. ASCII only. No single-letter names. Reserved:

ability all also and any as at be break by can change check collect
concurrently continue count crash deprecated descending each end example
expose exposing fail fails failure false first for from function greater
group has if ignore import in is lazy least less let match maybe module most
mutable needs not nothing of one only or otherwise per power public purpose
raw remainder repeat replays return returns run see self some sorted success
sum tags test than then to tool true type until when where with within

Any word is valid after a dot.