Introduction
“You see, Vergon 6 was once filled with the super-dense substance known as dark matter, each pound of which weighs over 10,000 pounds.” — Futurama, S1E4
Numbat is a statically typed programming language for scientific computations with first class support for physical dimensions and units.
You can use it for simple mathematical computations:
>>> 1920/16*9
= 1080
>>> 2^32
= 4294967296
>>> sqrt(1.4^2 + 1.5^2) * cos(pi/3)^2
= 0.512957
The real strength of Numbat, however, is to perform calculations with physical units:
>>> 8 km / (1 h + 25 min)
8 kilometer / (1 hour + 25 minute)
= 5.64706 km/h [Velocity]
>>> 140 € -> GBP
140 euro ➞ british_pound
= 120.768 £ [Money]
>>> atan2(30 cm, 1 m) -> deg
atan2(30 centimeter, 1 meter) ➞ degree
= 16.6992°
>>> let ω = 2π c / 660 nm
let ω: Frequency = 2 π × c / 660 nanometer
>>> ℏ ω -> eV
ℏ × ω ➞ electronvolt
= 1.87855 eV [Energy]
Read the tutorial to learn more about the language. Or jump directly to the syntax reference.
Tutorial
In this tutorial, you will use Numbat to calculate how many bananas you would need to power a house. This is based on an article in the great what if? series by the author of the xkcd comics.
Bananas contain potassium. In its natural form, potassium contains a tiny fraction (0.0117%)
of the isotope 40K, which is radioactive. The idea is to
use the radioactive decay energy as a power source. Open an interactive Numbat session by
typing numbat
in your favorite terminal emulator. We start by entering a few facts about
potassium-40:
let halflife = 1.25 billion years
let occurrence = 0.0117%
let molar_mass = 40 g / mol
New constants are introduced with the let
keyword. We
define these physical quantities with their respective physical units (years
,
percent
, g / mol
) in order to profit from Numbat’s unit-safety and unit-conversion
features later on.
Our first goal is to compute the radioactivity of natural potassium. Instead of dealing with the
half-life, we want to know the decay rate. When entering the following computation, you can try
Numbat’s auto-completion functionality. Instead of typing out halflife
, just type half
and press
Tab
.
let decay_rate = ln(2) / halflife
As you can see, we can use typical mathematical functions such as the
natural logarithm ln
. Next, we are interested how much radioactivity comes from a certain
mass of potassium:
let radioactivity =
N_A * occurrence * decay_rate / molar_mass -> Bq / g
The -> Bq / g
part at the end converts the expression to Becquerel per gram. If you type
in radioactivity
, you should see a result of roughly 31 Bq / g
, i.e. 31 radioactive
decays per second, per gram of potassium.
The unit conversion also serves another purpose. If anything would be wrong with our calculation at the units-level, Numbat would detect that and show an error. Unit safety is a powerful concept not just because you can eliminate an entire category of errors, but also because it makes your computations more readable.
We are interested in the radioactivity of bananas, so we first introduce a new (base) unit:
unit banana
This lets us write readable code like
let potassium_per_banana = 451 mg / banana
let radioactivity_banana = potassium_per_banana * radioactivity -> Bq / banana
and should give you a result of roughly 14 Bq / banana
. Adding unit conversions at the end
of unit definitions is one way to enforce unit safety. An even more powerful way to do this
is to add type annotations: For example, to define the decay energy for a single
potassium-40 atom,
you can optionally add a : Energy
annotation that will be enforced by Numbat:
let energy_per_decay: Energy = 11 percent × 1.5 MeV + 89 percent × 1.3 MeV
This also works with custom units since Numbat adds new physical dimensions (types) implicitly:
let power_per_banana: Power / Banana = radioactivity_banana * energy_per_decay
You’ll also notice that types can be combined via mathematical operators such as /
in this example.
How many bananas we need to power a household is going to depend on the average power consumption of that household. So we are defining a simple function
fn household_power(annual_consumption: Energy) -> Power = annual_consumption / year
This allows us to finally answer the original question (for a typical US household in 2021)
household_power(10000 kWh) / power_per_banana
This should give you a result of roughly 4×1014 bananas1.
Attribution
The images in this tutorial are from https://what-if.xkcd.com/158/. They are licensed under the Creative Commons Attribution-NonCommercial 2.5 License. Details and usage notes can be found at https://xkcd.com/license.html.
Interestingly, the what if article comes up with a result of 300 quadrillion bananas, or 3 × 1017. This is a factor of 1000 higher. This seems like a mistake in the original source. All of our other intermediate results are consistent with what has been computed in the original article.
Examples
This chapter shows some exemplary Numbat programs from various disciplines and sciences.
Acidity
# Compute the pH (acidity) of a solution based
# on the activity of hydrogen ions
#
# https://en.wikipedia.org/wiki/PH
fn pH_acidity(activity_hplus: Molarity) -> Scalar =
- log10(activity_hplus / (mol / L))
print(pH_acidity(5e-6 mol / L))
Barometric formula
# This script calculates the air pressure at a specified
# height above sea level using the barometric formula.
let p0: Pressure = 1 atm
let t0: Temperature = 288.15 K
dimension TemperatureGradient = Temperature / Length
let lapse_rate: TemperatureGradient = 0.65 K / 100 m
fn air_pressure(height: Length) -> Pressure =
p0 · (1 - lapse_rate · height / t0)^5.255
print("Air pressure 1500 m above sea level: {air_pressure(1500 m) -> hPa}")
Body mass index
# This script calculates the Body Mass Index (BMI) based on
# the provided mass and height values.
unit BMI: Mass / Length^2 = kg / m^2
fn body_mass_index(mass: Mass, height: Length) =
mass / height² -> BMI
print(body_mass_index(70 kg, 1.75 m))
Factorial
# Naive factorial implementation to showcase recursive
# functions and conditionals.
fn factorial(n: Scalar) -> Scalar =
if n < 1
then 1
else n × factorial(n - 1)
# Compare result with the builtin factorial operator
assert_eq(factorial(10), 10!)
Flow rate in a pipe
# This script calculates and prints the flow rate in a pipe
# using the Hagen-Poiseuille equation. It assumes the dynamic
# viscosity of water and allows for inputs of pipe radius,
# pipe length, and pressure difference.
let μ_water: DynamicViscosity = 1 mPa·s
fn flow_rate(radius: Length, length: Length, Δp: Pressure) -> FlowRate =
π × radius^4 × Δp / (8 μ_water × length)
let pipe_radius = 1 cm
let pipe_length = 10 m
let Δp = 0.1 bar
let Q = flow_rate(pipe_radius, pipe_length, Δp)
print("Flow rate: {Q -> L/s}")
Medication dosage
# This script calculates the total daily dose and per intake
# dose of a medication based on a person's body weight.
@aliases(takings)
unit taking
let body_weight = 75 kg
let dosage = (60 mg / kg) / day
let frequency = 3 takings / day
let total_daily_dose = dosage * body_weight -> mg / day
print("Total daily dose: {total_daily_dose}")
let single_dose = total_daily_dose / frequency
print("Single dose: {single_dose}")
Molarity
# This script calculates and prints the molarity of a salt
# water solution, given a fixed mass of salt (NaCl) and a
# volume of water.
let molar_mass_NaCl = 58.44 g / mol
fn molarity(mass: Mass, volume: Volume) -> Molarity =
(mass / molar_mass_NaCl) / volume
let salt_mass = 9 g
let water_volume = 1 L
print(molarity(salt_mass, water_volume) -> mmol / l)
Musical note frequency
# Musical note frequencies in the 12 equal temperament system
let frequency_A4: Frequency = 440 Hz # the A above middle C, A4
fn note_frequency(n: Scalar) -> Frequency = frequency_A4 * 2^(n / 12)
print("A5: {note_frequency(12)}") # one octave higher up, 880 Hz
print("E4: {note_frequency(7)}")
print("C4: {note_frequency(-3)}")
Population growth
# Exponential model for population growth
let initial_population = 50_000 people
let growth_rate = 2% per year
fn predict_population(t: Time) =
initial_population × e^(growth_rate·t) // round
print("Population in 20 years: {predict_population(20 years)}")
print("Population in 100 years: {predict_population(1 century)}")
Recipe
# Scale ingredient quantities based on desired servings.
@aliases(servings)
unit serving
let original_recipe_servings = 2 servings
let desired_servings = 3 servings
fn scale<D>(quantity: D) -> D =
quantity × desired_servings / original_recipe_servings
print("Milk: {scale(500 ml)}")
print("Flour: {scale(250 g)}")
print("Sugar: {scale(2 cups)}")
print("Baking powder: {scale(4 tablespoons)}")
XKCD 687
# Dimensional analysis
#
# https://xkcd.com/687/
let core_pressure = 3.5 million atmospheres
let prius_milage = 50 miles per gallon
let min_width_channel = 21 miles
# Make sure that the result is dimensionless:
let r: Scalar =
planck_energy / core_pressure × prius_milage / min_width_channel
print("{r} ≈ π ?")
XKCD 2585
# Rounding
#
# https://xkcd.com/2585/
17 mph
ans -> meters/sec // round
ans -> knots // round
ans -> fathoms/sec // round
ans -> furlongs/min // round
ans -> fathoms/sec // round
ans -> kph // round
ans -> knots // round
ans -> kph // round
ans -> furlongs/hour // round
ans -> mph // round
ans -> m/s // round
ans -> furlongs/min // round
ans -> yards/sec // round
ans -> fathoms/sec // round
ans -> m/s // round
ans -> mph // round
ans -> furlongs/min // round
ans -> knots // round
ans -> yards/sec // round
ans -> fathoms/sec // round
ans -> knots // round
ans -> furlongs/min // round
ans -> mph // round
print("I can ride my bike at {ans}.")
print("If you round.")
XKCD 2812
# Solar panel placement
#
# Solar energy tip: To maximize sun exposure, always
# orient your panels downward and install them on the
# surface of the sun.
#
# https://xkcd.com/2812/
#
# [1] https://en.wikipedia.org/wiki/Solar_luminosity
# [2] https://en.wikipedia.org/wiki/Sun
unit $: Money
let net_metering_rate = $ 0.20 / kWh
let panel_area = 1 m²
let panel_efficiency = 20 %
fn savings(i: Irradiance) -> Money / Time =
net_metering_rate × i × panel_area × panel_efficiency -> $/year
print("Option A: On the roof, south facing")
let savings_a = savings(4 kWh/m²/day)
print(savings_a // round)
print()
print("Option B: On the sun, downward facing")
dimension Luminosity = Power
let sun_luminosity: Luminosity = 3.828e26 W # [1]
let sun_area: Area = 6.09e12 km^2 # [2]
let savings_b = savings(sun_luminosity / sun_area)
print(savings_b // round)
IDE / editor integration
There is syntax highlighting support for the following IDEs / text editors:
Basics
This chapter introduces language features that are required to perform basic computations in Numbat or to write small programs.
Number notation
Numbers in Numbat can be written in the following forms:
- Integer notation
12345
12_345
— with decimal separators
- Floating point notation
0.234
.234
— without the leading zero
- Scientific notation
1.234e15
1.234e+15
1e-9
1.0e-9
- Non-decimal bases notation
0x2A
— Hexadecimal0o52
— Octal0b101010
— Binary
Unit notation
Most units can be entered in the same way that they would appear in textbook calculations. They
usually have a long form (meter
, degrees
, byte
, …), a plural form (meters
, degrees
, bytes
),
and a short alias (m
, °
, B
). For a full list of supported units, see
this page.
All SI-accepted units support metric prefixes (mm
, cm
, km
, … or millimeter
, centimeter
, kilometer
, …)
and — where sensible — units allow for binary prefixes (MiB
, GiB
, … or mebibyte
, gibibyte
, …). Note
that the short-form prefixes can only be used with the short version of the unit, and vice versa (that is: kmeter
and kilom
are not allowed, only km
and kilometer
).
Units can be combined using mathematical operations such as multiplication, division and exponentiation: kg * m/s^2
, km/h
, m²
, meter per second
.
The following snippet shows various styles of entering units:
2 min + 1 s
150 cm
sin(30°)
50 mph
6 MiB
2 minutes + 1 second
150 centimeters
sin(30 degrees)
50 miles per hour
6 mebibyte
Note that Numbat also allows you to define new units.
Operations and precedence
Numbat operators and other language constructs, ordered by precedence form high to low:
Operation / operator | Syntax |
---|---|
square, cube, … | x² , x³ , x⁻¹ , … |
factorial | x! |
exponentiation | x^y , x**y |
multiplication (implicit) | x y (whitespace) |
unary negation | -x |
division | x per y |
division | x / y , x ÷ y |
multiplication (explicit) | x * y , x · y , x × y |
subtraction | x - y |
addition | x + y |
comparisons | x < y , x <= y , x ≤ y , … x == y , x != y |
unit conversion | x -> y , x → y , x ➞ y , x to y |
conditionals | if x then y else z |
reverse function call | x // f |
Note that implicit multiplication has a higher precedence than division, i.e. 50 cm / 2 m
will be parsed as 50 cm / (2 m)
.
Also, note that per
-division has a higher precedence than /
-division. This means 1 / meter per second
will be parsed as 1 / (meter per second)
.
If in doubt, you can always look at the pretty-printing output (second line in the snippet below) to make sure that your input was parsed correctly:
>>> 1 / meter per second
1 / (meter / second)
= 1 s/m
Constants
New constants can be introduced with the let
keyword:
let pipe_radius = 1 cm
let pipe_length = 10 m
let Δp = 0.1 bar
Definitions may contain a type annotation after the identifier (let Δp: Pressure = 0.1 bar
). This annotation will be verified by the type checker. For more complex definitions
it can be desirable to add type annotations, as it often improves readability and allows
you to catch potential errors early:
let μ_water: DynamicViscosity = 1 mPa·s
let Q: FlowRate = π × pipe_radius^4 × Δp / (8 μ_water × pipe_length)
Unit conversions
The conversion operator ->
attempts to convert the physical quantity on its left hand side to
the unit of the expression on its right hand side. This means that you can write an arbitrary
expression on the right hand side — but only the unit part will be extracted. For example:
# simple unit conversion:
> 120 km/h -> mph
= 74.5645 mi/h
# expression on the right hand side:
> 120 m^3 -> km * m^2
= 0.12 m²·km
# convert x1 to the same unit as x2:
> let x1 = 50 km / h
> let x2 = 3 m/s -> x1
x2 = 10.8 km/h
Function definitions
Numbat comes with a large number of predefined functions, but
it is also possible to add new functions. A function definition is introduced with
the fn
keyword:
fn max_distance(v: Velocity, θ: Angle) -> Length = v² · sin(2 θ) / g0
This exemplary function computes the maximum distance of a projectile under the
influence of Earths gravity. It takes two parameters (The initial velocity v
and
the launch angle θ
), which are both annotated with their corresponding physical
dimension (their type). The function returns a distance, and so the return type
is specified as Length
.
Type inference
The return type annotation may be omitted, but it is often desirable to add it for better readability of the code and in order to catch potential errors.
The parameter types can also (sometimes) be omitted, in which case Numbat tries to infer their type. However, this often leads to overly generic function signatures. For example, consider the following function to compute the kinetic energy of a massive object in motion:
fn kinetic_energy(mass, speed) = 1/2 * mass * speed^2
Without any type annotations, this function has an overly generic type where
mass
and speed
can have arbitrary dimensions (and the return type is
type(mass) * type(speed)^2
). So for this case, it is probably better to add
parameter and return types.
Generic functions
Sometimes however, it is useful to write generic functions. For example, consider
max(a, b)
— a function that returns the larger of the two arguments. We might
want to use that function with dimensionful arguments such as max(1 m, 1 yd)
.
To define such a generic function, you can introduce type parameters in angle
brackets:
fn max<T>(a: T, b: T) -> T = if a > b then a else b
This function signature tells us that max
takes two arguments of arbitrary
type T
(but they need to match!), and returns a quantity of the same type T
.
Note that you can perform the usual operations with type parameters, such as multiplying/dividing them with other types, or raising to rational powers. For example, consider this cube-root function
fn cube_root<T>(x: T^3) -> T = x^(1/3)
that can be called with a scalar (cube_root(8) == 2
) or a dimensionful
argument (cube_root(1 liter) == 10 cm
).
Note: cube_root
can also be defined as fn cube_root<T>(x: T) -> T^(1/3)
,
which is equivalent to the definition above.
Recursive functions
It is also possible to define recursive functions. In order to do so, you currently need to specify the return type — as the type signature can not (yet) be inferred otherwise.
For example, a naive recursive implementation to compute Fibonacci numbers in Numbat looks like this:
fn fib(n: Scalar) -> Scalar =
if n ≤ 2
then 1
else fib(n - 2) + fib(n - 1)
Conditionals
Numbat has if-then-else
conditional expressions with the following
syntax
if <cond> then <expr1> else <expr2>
where <cond>
is a condition that evaluates to a Boolean value, like
3 ft < 3 m
. The types of <expr1>
and <expr2>
need to match.
For example, you can defined a simple step function using
fn step(x: Scalar) -> Scalar = if x < 0 then 0 else 1
Printing, testing, debugging
Printing
Numbat has a builtin print
procedure that can be used to print the value of an expression:
print(2 km/h)
print(3 ft < 1 m)
You can also print out simple messages as strings. This is particularly useful when combined with string interpolation to print results of a computation:
let radius: Length = sqrt(footballfield / 4 pi) -> meter
print("A football field would fit on a sphere of radius {radius}")
You can use almost every expression inside a string interpolation field. For example:
print("3² + 4² = {hypot2(3, 4)}²")
let speed = 25 km/h
print("Speed of the bicycle: {speed} ({speed -> mph})")
Testing
The assert_eq
procedure can be used to test for (approximate) equality of two quantities.
This is often useful to make sure that (intermediate) results in longer calculations have
a certain value, e.g. when restructuring the code. The general syntax is
assert_eq(q1, q2)
assert_eq(q1, q2, ε)
where the first version tests for exact equality while the second version tests for approximate equality \( |q_1-q_2| < \epsilon \) with a specified accuracy of \( \epsilon \). For example:
assert_eq(2 + 3, 5)
assert_eq(1 ft × 77 in², 4 gal)
assert_eq(alpha, 1 / 137, 1e-4)
assert_eq(3.3 ft, 1 m, 1 cm)
There is also a plain assert
procedure that can test any boolean condition. For example:
assert(1 yard < 1 meter)
assert(π != 3)
A runtime error is thrown if an assertion fails. Otherwise, nothing happens.
Debugging
You can use the builtin type
procedure to see the type (or physical dimension) of a quantity:
>>> type(g0)
Length / Time²
>>> type(2 < 3)
Bool
Advanced
This chapter covers more advanced topics, like defining custom physical units or new physical dimensions.
Unit definitions
New units of measurement can be introduced with the unit
keyword. There are two types of units: base units and derived units.
A new base unit can be defined by specifying the physical dimension it represents. For example, in the International System of Units (SI), the second is the base unit for measuring times:
unit second: Time
Here, Time
denotes the physical dimension. To learn more, you can read the corresponding chapter. But for now, we can just assume that they are already given.
Derived units are also introduced with the unit
keyword. But unlike base units, they are defined through their relation to
other units. For example, a minute can be defined as
unit minute: Time = 60 second
Here, the : Time
annotation is optional. If a dimension is specified, it will be used to verify that the right hand side expression (60 second
) is indeed of physical dimension Time
. This is apparent in this simple example, but can be useful for more complicated unit definitions like
unit farad: Capacitance = ampere^2 second^4 / (kilogram meter^2)
Prefixes
If a unit may be used with metric prefixes such as milli
/m
, kilo
/k
or mega
/M
, we can prepend the unit definition with the @metric_prefixes
decorator:
@metric_prefixes
unit second: Time
This allows identifiers such as millisecond
to be used in calculations. See the section below how prefixes interact with aliases.
Similarly, if a unit should be prependable with binary (IEC) prefixes such as kibi
/Ki
, mebi
/Mi
or gibi
/Gi
, you can
add the @binary_prefixes
decorator. A unit might also allow for both metric and binary prefixes, for example:
@binary_prefixes
@metric_prefixes
unit byte = 8 bit
This allows the usage of both mebibyte
(1024² byte) as well as megabyte
(1000² byte).
Aliases
It is often useful to define alternative names for a unit. For example, we might want to use the plural form seconds
or the commonly
used short version s
. We can use the @aliases
decorator to specify them:
@metric_prefixes
@aliases(meters, metre, metres, m: short)
unit meter: Length
In addition to the name, we can also specify how aliases interact with prefixes using : long
(the default), : short
, : both
or
: none
. The actual unit name (meter
) and all long
aliases will accept the long version of prefixes (…, milli
, kilo
, mega
, giga
, …).
All short
aliases (m
in the example above) will only accept the respective short versions of the prefixes (…, m
, k
, M
, G
, …).
Aliases annotated with : both
or : none
accept either both long and short prefixes, or none of them.
The unit definition above allows all of following expressions:
millimeter
kilometer
millimeters
kilometers
millimetre
kilometre
millimetres
kilometres
mm
km
...
Dimension definitions
New (physical) dimensions can be introduced with the dimension
keyword. Similar to units, there are base dimensions (like length, time and mass) and dimensions that are derived from those base dimensions (like momentum, which is mass · length / time). Base dimensions are simply introduced by declaring their name:
dimension Length
dimension Time
dimension Mass
Derived dimensions need to specify their relation to base dimensions (or other derived dimensions). For example:
dimension Velocity = Length / Time
dimension Momentum = Mass * Velocity
dimension Force = Mass * Acceleration = Momentum / Time
dimension Energy = Momentum^2 / Mass = Mass * Velocity^2 = Force * Length
In the definition of Force
and Energy
, we can see that alternative definitions can be given. This is entirely optional. If specified, the compiler will make sure that all definitions are equivalent.
Custom dimensions
It is often useful to introduce ‘fictional’ physical dimensions. For example, we might want to do calculations with
screen resolutions and ‘dot densities’. Introducing a new dimension for dots then allows us to define units like dpi
without sacrificing unit safety:
dimension Dot
@aliases(dots)
unit dot: Dot
unit dpi = dots / inch
fn inter_dot_spacing(resolution: Dot / Length) -> Length = 1 dot / resolution
inter_dot_spacing(72 dpi) -> µm # 353 µm
There is also a shorthand notation for creating a new dimension and a corresponding unit:
unit book
@aliases(pages)
unit page
@aliases(words)
unit word
let words_per_book = 500 words/page × 300 pages/book
Here, the base unit definitions will implicitly create new dimensions which are capitalized
versions of the unit names (Book
, Page
, Word
). This allows you to count books, pages
and words independently without any risk of mixing them. The words_per_book
constant in this
examples has a type of Word / Book
.
Syntax overview
# This is a line comment. It can span over
# multiple lines
# 1. Imports
use prelude # This is not necessary. The 'prelude'
# module will always be loaded upon startup
use units::stoney # Load a specific module
# 2. Numbers
12345 # integer notation
12_345 # optional decimal separators
0.234 # floating point notation
.234 # without the leading zero
1.234e15 # scientific notation
1.234e+15
1e-9
1.0e-9
0x2A # hexadecimal
0o52 # octal
0b101010 # binary
# 3. Simple expressions
3 + (4 - 3) # Addition and subtraction
1920 / 16 * 9 # Multiplication, division
1920 ÷ 16 × 9 # Unicode-style, '·' is also multiplication
2 pi # Whitespace is implicit multiplication
meter per second # 'per' keyword can be used for division
2^3 # Exponentiation
2**3 # Python-style
2³ # Unicode exponents
2^-3 # Negative exponents
mod(17, 4) # Modulo
3 in -> cm # Unit conversion, can also be → or ➞
3 in to cm # Unit conversion with the 'to' keyword
cos(pi/3 + pi) # Call mathematical functions
pi/3 + pi // cos # Same, 'arg // f' is equivalent to 'f(arg)'
# The '//' operator has the lowest precedence
# which makes it very useful for interactive
# terminals (press up-arrow, and add '// f')
# 4. Constants
let n = 4 # Simple numerical constant
let q1 = 2 m/s # Right hand side can be any expression
let q2: Velocity = 2 m/s # With optional type annotation
let q3: Length / Time = 2 m/s # more complex type annotation
# 5. Function definitions
fn foo(z: Scalar) -> Scalar = 2 * z + 3 # A simple function
fn speed(len: Length, dur: Time) -> Velocity = len / dur # Two parameters
fn my_sqrt<T>(q: T^2) -> T = q^(1/2) # A generic function
fn is_non_negative(x: Scalar) -> Bool = x ≥ 0 # Returns a bool
# 6. Dimension definitions
dimension Fame # A new base dimension
dimension Deceleration = Length / Time^2 # A new derived dimension
# 7. Unit definitions
@aliases(quorks) # Optional aliases-decorator
unit quork = 0.35 meter # A new derived unit
@metric_prefixes # Optional decorator to allow 'milliclonk', etc.
@aliases(ck: short) # short aliases can be used with short prefixes (mck)
unit clonk: Time = 0.2 seconds # Optional type annotation
@metric_prefixes
@aliases(wh: short)
unit warhol: Fame # New base unit for the "Fame" dimension
unit thing # New base unit with automatically generated
# base dimension "Thing"
# 8. Conditionals
fn step(x: Scalar) -> Scalar = # The construct 'if <cond> then <expr> else <expr>'
if x < 0 # is an expression, not a statement. It can span
then 0 # multiple lines.
else 1
# 9. Procedures
print(2 kilowarhol) # Print the value of an expression
print("hello world") # Print a message
print("value of pi = {pi}") # String interpolation
print("sqrt(10) = {sqrt(10)}") # Expressions in string interpolation
assert(1 yard < 1 meter) # Assertion
assert_eq(1 ft, 12 in) # Assert that two quantities are equal
assert_eq(1 yd, 1 m, 10 cm) # Assert that two quantities are equal, up to
# the given precision
type(2 m/s) # Print the type of an expression
The prelude
Numbat comes with a special module called prelude
that is always loaded on
startup (unless --no-prelude
is specified on the command line). This module
is split into multiple submodules and sets up a useful default environment with
mathematical functions, constants but also dimension definitions, unit
definitions and physical constants.
You can find the full source code of the standard library on GitHub.
This chapter is a reference to the prelude module.
Predefined functions
Utility
fn unit_of<T>(x: T) -> T
fn value_of<T>(x: T) -> Scalar
Math
Basics
fn abs<T>(x: T) -> T
fn round<T>(x: T) -> T
fn floor<T>(x: T) -> T
fn ceil<T>(x: T) -> T
fn mod<T>(a: T, b: T) -> T
fn sqrt<D>(x: D^2) -> D
fn sqr<D>(x: D) -> D^2
Exponential and logarithm
fn exp(x: Scalar) -> Scalar
fn ln(x: Scalar) -> Scalar
fn log(x: Scalar) -> Scalar
fn log10(x: Scalar) -> Scalar
fn log2(x: Scalar) -> Scalar
Trigonometry
Basic:
fn cos(x: Scalar) -> Scalar
fn sin(x: Scalar) -> Scalar
fn tan(x: Scalar) -> Scalar
fn asin(x: Scalar) -> Scalar
fn acos(x: Scalar) -> Scalar
fn atan(x: Scalar) -> Scalar
fn atan2<T>(y: T, x: T) -> Scalar
Hyperbolic:
fn sinh(x: Scalar) -> Scalar
fn cosh(x: Scalar) -> Scalar
fn tanh(x: Scalar) -> Scalar
fn asinh(x: Scalar) -> Scalar
fn acosh(x: Scalar) -> Scalar
fn atanh(x: Scalar) -> Scalar
Extra:
fn cot(x: Scalar) -> Scalar
fn acot(x: Scalar) -> Scalar
fn coth(x: Scalar) -> Scalar
fn acoth(x: Scalar) -> Scalar
fn secant(x: Scalar) -> Scalar
fn arcsecant(x: Scalar) -> Scalar
fn cosecant(x: Scalar) -> Scalar
fn csc(x: Scalar) -> Scalar
fn acsc(x: Scalar) -> Scalar
fn sech(x: Scalar) -> Scalar
fn asech(x: Scalar) -> Scalar
fn csch(x: Scalar) -> Scalar
fn acsch(x: Scalar) -> Scalar
Others
fn gamma(x: Scalar) -> Scalar
Statistics
fn mean<D>(xs: D…) -> D
fn maximum<D>(xs: D…) -> D
fn minimum<D>(xs: D…) -> D
Geometry
fn hypot2<T>(x: T, y: T) -> T
fn hypot3<T>(x: T, y: T, z: T) -> T
fn circle_area<L>(radius: L) -> L^2
fn circle_circumference<L>(radius: L) -> L
fn sphere_area<L>(radius: L) -> L^2
fn sphere_volume<L>(radius: L) -> L^3
Physics
Temperature conversion
fn from_celsius(t_celsius: Scalar) -> Temperature
fn to_celsius(t_kelvin: Temperature) -> Scalar
fn from_fahrenheit(t_fahrenheit: Scalar) -> Temperature
fn to_fahrenheit(t_kelvin: Temperature) -> Scalar
Strings
fn str_length(s: String) -> Scalar
fn str_slice(s: String, start: Scalar, end: Scalar) -> String
fn str_append(a: String, b: String) -> String
fn str_contains(haystack: String, needle: String) -> Bool
fn str_replace(s: String, pattern: String, replacement: String) -> String
fn str_repeat(a: String, n: Scalar) -> String
Constants
Mathematical
pi
,π
τ
e
golden_ratio
,φ
Named numbers
Large numbers
hundred
thousand
million
billion
trillion
quadrillion
quintillion
googol
Unicode fractions:
½
,⅓
,⅔
,¼
,¾
, …
Colloquial:
quarter
half
semi
double
triple
dozen
Physics
Description | Identifier | Dimension |
---|---|---|
The speed of light in vacuum | speed_of_light , c | Velocity |
The Newtonian constant of gravitation | gravitational_constant , G | Force × Length^2 / Mass^2 |
Standard acceleration of gravity on earth | gravity , g0 | Acceleration |
The Planck constant | planck_constant , ℎ | Mass × Length^2 / Time |
The reduced Planck constant | h_bar , ℏ | Mass × Length^2 / Time |
Mass of the electron | electron_mass | Mass |
Elementary charge (charge of the electron) | elementary_charge , electron_charge | ElectricCharge |
Magnetic constant (vacuum magnetic permeability) | magnetic_constant , µ0 , mu0 | Force / Current^2 |
Electric constant (vacuum electric permittivity) | electric_constant , ε0 , eps0 | Capacitance / Length |
Bohr magneton | bohr_magneton , µ_B | Energy / MagneticFluxDensity |
Fine structure constant | fine_structure_constant , alpha , α | Scalar |
Proton mass | proton_mass | Mass |
Neutron mass | neutron_mass | Mass |
Avogadro constant | avogadro_constant , N_A | 1 / AmountOfSubstance |
Boltzmann constant | boltzmann_constant , k_B | Energy / Temperature |
Stefan-Boltzmann constant | stefan_boltzmann_constant | Power / (Area × Temperature^4) |
Ideal gas constant | gas_constant , R | Energy / (AmountOfSubstance × Temperature) |
Planck length | planck_length | Length |
Planck mass | planck_mass | Mass |
Planck time | planck_time | Time |
Planck temperature | planck_temperature | Temperature |
Planck energy | planck_energy | Energy |
Bohr radius | bohr_radius , a0 | Length |
Rydberg constant | rydberg_constant | Wavenumber |
List of supported units
See also: Unit notation.
All SI-accepted units support metric prefixes (mm
, cm
, km
, … or millimeter
, centimeter
, kilometer
, …)
and — where sensible — units allow for binary prefixes (MiB
, GiB
, … or mebibyte
, gibibyte
, …).
Dimension | Unit name | Identifier(s) |
---|---|---|
AbsorbedDose | Gray | gray , grays , Gy |
Activity | Becquerel | becquerel , becquerels , Bq |
AmountOfSubstance | Mole | mol , mole , moles |
Angle | Minute of arc | arcmin , arcminute , arcminutes |
Angle | Second of arc | arcsec , arcsecond , arcseconds |
Angle | Degree | deg , degree , degrees , ° |
Angle | Gradian | gon |
Angle | Radian | rad , radian , radians |
Angle | Revolution | rev , revolution , revolutions |
Angle | Turn | turn , turns |
Area | Acre | acre , acres |
Area | Are | are |
Area | Barn | barn , barns |
Area | Football field | footballfield |
Area | Hectare | ha , hectare , hectares |
Capacitance | Farad | F , farad , farads |
CatalyticActivity | Katal | kat , katal , katals |
Current | Ampere | A , ampere , amperes |
DigitalInformation | Bit | bit , bits |
DigitalInformation | Byte | B , byte , Byte , bytes , Bytes , octet , Octet , octets , Octets |
DigitalInformation / Time | Bits per second | bps |
Dot | Dot | dot , dots |
Dot / Length | Dots per inch | dpi |
DynamicViscosity | Poise | poise |
ElectricCharge | Coulomb | C , coulomb , coulombs |
ElectricConductance | Siemens | S , siemens |
ElectricResistance | Ohm | ohm , ohms , Ω |
Energy | British thermal unit | BTU , Btu |
Energy | Calorie | cal , calorie , calories |
Energy | Electronvolt | electronvolt , electronvolts , eV |
Energy | Erg | erg , ergs |
Energy | Hartree | hartree , hartrees |
Energy | Joule | J , joule , joules |
Energy | Planck energy | planck_energy |
Energy | Rydberg unit of energy | Ry |
Energy | Watt-hour | watthour , Wh |
EquivalentDose | Sievert | sievert , sieverts , Sv |
Force | Dyne | dyn , dyne |
Force | Kilogram-force | kgf , kilogram_force |
Force | Newton | N , newton , newtons |
Force | Ounce-force | ounce_force , ozf |
Force | Pound-force | lbf , pound_force |
Force / Volume | Mercury | Hg |
Frame | Frame | frame , frames |
Frame / Time | Frames per second | fps |
Frequency | Hertz | hertz , Hz |
Frequency | Revolutions per minute | rpm , RPM |
Illuminance | Foot-candle | fc , footcandle , footcandles |
Illuminance | Lux | lux , lx |
Inductance | Henry | H , henries , henry , henrys |
KinematicViscosity | Stokes | St , stokes |
Length | Ångström | angstrom , angstroms , Å |
Length | Astronomical unit | astronomicalunit , astronomicalunits , au , AU |
Length | Bohr | bohr |
Length | Fathom | fathom , fathoms |
Length | Fermi | fermi |
Length | Foot | feet , foot , ft |
Length | Furlong | furlong , furlongs |
Length | Inch | in , inch , inches |
Length | League | league , leagues |
Length | Light-year | lightyear , lightyears , ly |
Length | Metre | m , meter , meters , metre , metres |
Length | Micron | micron |
Length | Mile | mi , mile , miles |
Length | Nautical Mile | nautical_mile , nautical_miles , NM , nmi |
Length | Parsec | parsec , parsecs , pc |
Length | Planck length | planck_length |
Length | US rod | perch , rod , rods |
Length | Smoot | smoot |
Length | Stoney length | stoney_length |
Length | Thousandth of an inch | mil , mils , thou |
Length | Yard | yard , yards , yd |
Length / Volume | Miles per gallon | mpg |
LuminousFlux | Lumen | lm , lumen , lumens |
LuminousIntensity | Candela | candela , candelas , cd |
MagneticFieldStrength | Oersted | Oe , oersted |
MagneticFlux | Maxwell | maxwell , Mx |
MagneticFlux | Weber | Wb , weber , webers |
MagneticFluxDensity | Gauss | gauss |
MagneticFluxDensity | Tesla | T , tesla , teslas |
Mass | Dalton | Da , dalton , daltons |
Mass | Firkin | firkin , firkins |
Mass | Grain | grain , grains |
Mass | Gram | g , gram , gramme , grammes , grams |
Mass | Hundredweight | cwt , long_hundredweight |
Mass | Long ton | long_ton , long_tons |
Mass | Ounce | ounce , ounces , oz |
Mass | Planck mass | planck_mass |
Mass | Pound | lb , lbs , pound , pounds |
Mass | Stone | stone |
Mass | Stoney mass | stoney_mass |
Mass | Tonne | metricton , ton , tonne , tonnes , tons |
Molality | Molal | molal |
Molarity | Molar | molar |
Money | Australian dollar | A$ , AUD , australian_dollar , australian_dollars |
Money | Brazilian real | brazilian_real , brazilian_reals , BRL , R$ |
Money | Pound sterling | british_pound , GBP , pound_sterling , £ |
Money | Bulgarian lev | BGN , bulgarian_lev , bulgarian_leva |
Money | Canadian dollar | C$ , CAD , canadian_dollar , canadian_dollars |
Money | Czech koruna | czech_koruna , czech_korunas , CZK , Kč |
Money | Danish krone | danish_krone , danish_kroner , DKK |
Money | US dollar | $ , dollar , dollars , USD |
Money | Euro | EUR , euro , euros , € |
Money | Hong Kong dollar | HK$ , HKD , hong_kong_dollar , hong_kong_dollars |
Money | Hungarian forint | Ft , HUF , hungarian_forint , hungarian_forints |
Money | Icelandic króna | icelandic_krona , icelandic_kronur , icelandic_króna , icelandic_krónur , ISK |
Money | Indian rupee | indian_rupee , indian_rupees , INR , ₹ |
Money | Indonesian rupiah | IDR , indonesian_rupiah , indonesian_rupiahs , Rp |
Money | Israeli new shekel | ILS , israeli_new_shekel , israeli_new_shekels , NIS , ₪ |
Money | Malaysian ringgit | malaysian_ringgit , malaysian_ringgits , MYR , RM |
Money | New Zealand dollar | new_zealand_dollar , new_zealand_dollars , NZ$ , NZD |
Money | Norwegian krone | NOK , norwegian_krone , norwegian_kroner |
Money | Philippine peso | philippine_peso , philippine_pesos , PHP , ₱ |
Money | Polish złoty | PLN , polish_zloty , polish_zlotys , zł |
Money | Chinese yuan | CNY , renminbi , 元 |
Money | Romanian leu | lei , romanian_leu , romanian_leus , RON |
Money | Singapore dollar | S$ , SGD , singapore_dollar , singapore_dollars |
Money | South African rand | south_african_rand , ZAR |
Money | South Korean won | KRW , south_korean_won , south_korean_wons , ₩ |
Money | Swedish krona | SEK , swedish_krona , swedish_kronor |
Money | Swiss franc | CHF , swiss_franc , swiss_francs |
Money | Thai baht | thai_baht , thai_bahts , THB , ฿ |
Money | Turkish lira | TRY , turkish_lira , turkish_liras , ₺ |
Money | Japanese yen | JPY , yen , yens , ¥ , 円 |
Person | Person | capita , people , person , persons |
Piece | Piece | piece , pieces |
Pixel | Pixel | pixel , pixels , px |
Pixel / Length | Pixels per inch | ppi |
Power | Metric horsepower | horsepower , hp |
Power | Watt | W , watt , watts |
Pressure | Standard atmosphere | atm , atmosphere , atmospheres |
Pressure | Bar | bar , bars |
Pressure | Inch of mercury | inHg |
Pressure | Millimeter of mercury | mmHg |
Pressure | Pascal | Pa , pascal , pascals |
Pressure | Pound-force per square inch | psi , PSI |
Pressure | Torr | torr |
Scalar | Billion | billion |
Scalar | dozen | dozen |
Scalar | Hundred | hundred |
Scalar | Million | million |
Scalar | Parts per billion | partsperbillion , ppb |
Scalar | Parts per million | partspermillion , ppm |
Scalar | Parts per quadrillion | partsperquadrillion , ppq |
Scalar | Parts per trillion | partspertrillion , ppt |
Scalar | Percent | % , pct , percent |
Scalar | Quadrillion | quadrillion |
Scalar | Quintillion | quintillion |
Scalar | Thousand | thousand |
Scalar | Trillion | trillion |
SolidAngle | Steradian | sr , steradian , steradians |
Temperature | Kelvin | K , kelvin , kelvins |
Temperature | Planck temperature | planck_temperature |
Time | Century | centuries , century |
Time | Day | d , day , days |
Time | Decade | decade , decades |
Time | Fortnight | fortnight , fortnights |
Time | Hour | h , hour , hours , hr |
Time | Julian year | julian_year , julian_years |
Time | Millennium | millennia , millennium |
Time | Minute | min , minute , minutes |
Time | Month | month , months |
Time | Planck time | planck_time |
Time | Second | s , sec , second , seconds |
Time | Sidereal day | sidereal_day , sidereal_days |
Time | Stoney time | stoney_time |
Time | Week | week , weeks |
Time | Gregorian year | year , years , yr |
Velocity | Knot | kn , knot , knots , kt |
Velocity | Kilometres per hour | kph |
Velocity | Miles per hour | mph |
Voltage | Volt | V , volt , volts |
Volume | Cubic centimetre | cc , ccm |
Volume | US cup | cup , cups |
Volume | US fluid ounce | floz , fluidounce , fluidounces |
Volume | US liquid gallon | gal , gallon , gallons |
Volume | US hogshead | hogshead , hogsheads |
Volume | Litre | l , L , liter , liters , litre , litres |
Volume | US liquid pint | pint , pints |
Volume | Swimming pool | swimmingpool |
Volume | US tablespoon | tablespoon , tablespoons , tbsp |
Volume | US teaspoon | teaspoon , teaspoons , tsp |
Installation
Linux
Ubuntu
… and other Debian-based Linux distributions.
Download the latest .deb
package from the release page
and install it via dpkg
. For example:
curl -LO https://github.com/sharkdp/numbat/releases/download/v1.8.0/numbat_1.8.0_amd64.deb
sudo dpkg -i numbat_1.8.0_amd64.deb
Arch Linux
In Arch Linux and Arch based distributions, you can install the prebuilt package of Numbat from the AUR:
yay -S numbat-bin
You can also install the numbat AUR package, which will download the source and compile it.
yay -S numbat
Void Linux
You can install the numbat
package using
sudo xbps-install -S numbat
NixOs
… or any distribution where nix is installed.
Install numbat to your profile:
nix-env -iA nixpkgs.numbat
Or add it to your NixOs Configuration:
environment.systemPackages = [
pkgs.numbat
];
macOS
Homebrew
You can install Numbat with Homebrew:
brew install numbat
Windows
Scoop
You can install the numbat package using scoop:
scoop install main/numbat
From pre-built binaries
Download the latest release for your system from this page. Unpack
the archive and place the numbat
/numbat.exe
binary in a folder that is on your PATH
.
Note that the modules
folder that is included in the archives is not strictly required to run Numbat. It serves more
as a reference for interested users. However, if you want to get the best possible experience or if you are
a package maintainer, please follow these guidelines.
From source
Clone the Git repository, and build Numbat with cargo
:
git clone https://github.com/sharkdp/numbat
cd numbat/
cargo install -f --path numbat-cli
Or install the latest release using
cargo install numbat-cli
Guidelines for package maintainers
Thank you for packaging Numbat! This section contains instructions that are not strictly necessary to create a Numbat package, but provide users with the best-possible experience on your target platform.
Numbat has a standard library that is written in Numbat itself. The sources for this
so called “prelude” are available in the numbat/modules
folder.
We also include this modules
folder in the pre-built GitHub releases.
Installing this folder as part of the package installation is not necessary for Numbat to work, as the prelude is also
stored inside the numbat
binary. But ideally, this folder should be made available for users. There are three reasons for this:
- Users might want to look at the code in the standard library to get a better understanding of the language itself.
- For some error messages, Numbat refers to locations in the source code. For example, if you type
let meter = 2
, the compiler will let you know that this identifier is already in use, and has been previously defined at a certain location inside the standard library. If the corresponding module is available as a file on the users system, they will see the proper path and can read the corresponding file. - Users might want to make changes to the prelude. Ideally, this should be done via a user module folder, but the system-wide folder can serve as a template.
In order for this to work, the modules
folder should ideally be placed in the standard location for the
target operating system. If this is not possible, package maintainers can customize
numbat during compilation by setting the environment variable NUMBAT_SYSTEM_MODULE_PATH
to the final locatiom.
If this variable is set during compilation, the specified path will be compiled into the numbat
binary.
In order to test that everything is working as intended, you can open numbat
and type let meter = 2
. The
path in the error message should point to the specified location (and not to <builtin>/…
).
Usage
Modes
You can run the Numbat command-line application in three different modes:
Mode | Command to run |
---|---|
Start an interactive session (REPL) | numbat |
Run a Numbat program | numbat script.nbt |
Evaluate a single expression | numbat -e '30 km/h -> mi/h' |
Command-line options
See numbat --help
for more information.
Interactive sessions
Interactive sessions allow you to perform a sequence of calculations. You can use the special identifiers
ans
or _
to refer to the result of the last calculation. For example:
>>> 60 kW h / 150 kW
= 0.4 h
>>> ans -> minutes
= 24 min
Commands
There is a set of special commands that only work in interactive mode:
Command | Action |
---|---|
list , ls | List all functions, dimensions, variables and units |
list <what> | Where <what> can be functions , dimensions , variables , units |
info <identifier> | Get more information about units and variables |
clear | Clear screen |
help , ? | View short help text |
quit , exit | Quit the session |
Key bindings
In interactive command-line mode, you can use the following key bindings. Most importantly,
Tab
for auto-completion, arrow keys and Ctrl-R
for browsing the command history, and
Ctrl-D
for exiting the interactive session.
Key sequence | Action |
---|---|
Tab , Ctrl -I | Auto-completion |
Ctrl -D | Quit |
Ctrl -L | Clear screen |
Up , Down | Browse command history |
Ctrl -R | Search command history |
Ctrl -C | Clear the current line |
Alt -Enter | Insert newline |
Home , Ctrl -A | Move cursor to the beginning of the line |
End , Ctrl -E | Move cursor to the end of the line |
Ctrl -W | Delete word leading up to cursor |
Customization
Startup
By default, Numbat will load the following modules/files during startup, in order:
- Numbat Prelude (a module called
prelude
, either from<module-path>/prelude.nbt
if available, or the builtin version) - The user initialization file, if available (a file called
init.nbt
from<config-path>/init.nbt
)
Config path
Numbat’s configuration folder (<config-path>
above) can be found under:
Platform | Path |
---|---|
Linux | $HOME/.config/numbat or $XDG_CONFIG_HOME/numbat |
macOS | $HOME/Library/Application Support/numbat |
Windows | C:\Users\Alice\AppData\Roaming\numbat |
Module paths
Numbat will load modules from the following sources. Entries higher up in the list take precedence.
Location | Description |
---|---|
$NUMBAT_MODULES_PATH | This environment variable can point to a single directory or contain a : -separatedlist of paths |
<config-path>/modules | User-customized module folder |
/usr/share/numbat/modules | System-wide module folder (Linux and macOS) |
C:\Program Files\numbat\modules | System-wide module folder (Windows) |
<builtin> | Builtin modules inside the numbat binary |
Note that the System-location might be different for some installation methods. Refer to your package manager for details.
Customization
Configuration
Numbat’s configuration file is called config.toml
, and it needs to be placed in
<config-path>
described above (~/.config/numbat/config.toml
on Linux). You
can generate a default configuration by calling
numbat --generate-config
The most important fields are:
# Controls the welcome message. Can be "long", "short", or "off".
intro-banner = "long"
# Controls the prompt character(s) in the interactive terminal.
prompt = ">>> "
# Whether or not to pretty-print expressions before showing the result.
# Can be "always", "never" or "auto". The latter uses pretty-printing
# only in interactive mode.
pretty-print = "auto"
[exchange-rates]
# When and if to load exchange rates from the European Central Bank for
# currency conversions. Can be "on-startup" to always fetch exchange rates
# in the background when the application is started. With "on-first-use",
# Numbat only fetches exchange rates when they are needed. Exchange rate
# fetching can also be disabled using "never". The latter will lead to
# "unknown identifier" errors when a currency unit is being used.
fetching-policy = "on-startup"
Custom functions, constants, units
If you want to add custom functions, constants, or units to your default environment,
create a init.nbt
file in your config folder (~/.config/numbat/init.nbt
on Linux).
Custom modules
You can also create your own modules that can be loaded on demand. To this end,
create a new file, say <module-path>/user/finance.nbt
in one of the module folders
(e.g. ~/.config/numbat/modules/custom/finance.nbt
on Linux). This module can then be
loaded using
use custom::finance
in your Numbat scripts or in the REPL. You can also load custom modules from init.nbt
if you want to have them available all the time.
You can also organize modules into subfolders (e.g. <module-path>/custom/finance/functions.nbt
).
In that case, you can load them using
use custom::finance::functions
In fact, the custom
folder is just a convention to avoid name clashes with the
standard library.
Usage
The browser-based version of Numbat is available at https://numbat.dev/.
Interactive terminal
The terminal allows you to perform a sequence of calculations.
You can use the arrow keys to browse through the command history.
The special identifiers ans
and _
refer to the result of the last calculation. For example:
>>> 60 kW h / 150 kW
= 0.4 h
>>> ans -> minutes
= 24 min
Commands
There is a set of special commands that only work in the web version:
Command | Action |
---|---|
list , ls | List all constants, units, and dimensions |
list <what> | Where <what> can be functions , dimensions , variables , units |
info <identifier> | Get more information about units and variables |
help , ? | View short help text |
reset | Reset state (clear constants, functions, units, …) |
clear | Clear screen |
Key bindings
In interactive command-line mode, you can use the following key bindings. Most importantly,
Tab
for auto-completion, arrow keys and Ctrl-R
for browsing the command history, and
Ctrl-D
for exiting the interactive session.
Key sequence | Action |
---|---|
Tab | Auto-completion |
Ctrl -L | Clear screen |
Up , Down | Browse command history |
Ctrl -R | Search command history |
Ctrl -C | Clear the current line |
Shift -Enter | Insert newline |
Home , Ctrl -A | Move cursor to the beginning of the line |
End , Ctrl -E | Move cursor to the end of the line |
Ctrl -Left , Ctrl -Right | Move cursor one word left/right |
Ctrl -K | Remove text to the right of the cursor |
Ctrl -U | Remove text to the left of the cursor |
Sharing calculations
To share the result of a calculation with someone else, you can just copy the URL from
your browers address bar. As you enter new lines in the terminal, your input will be
appended to the URL to build up something like
https://numbat.dev/?q=let+P0+%3D+50_000+people%0A…
that you can just copy and share. To reset the state and clear the URL, use the reset
command (see above).
Type system
Numbat is a language with a special type system that treats physical dimensions as types.
A type checker infers types for every expression in the program and ensures that everything is correct in terms of physical dimensions, which implies correctness in terms of physical units.
For example, the expression 2 meter
has a type of Length
.
The expression 3 inch
also has a type of Length
.
The combined expression 2 meter + 3 inch
is therefore well-typed.
On the other hand, 2 meter + 3 second
is ill-typed, as 3 second
is of type Time
.
The type system is static which means that the correctness of a Numbat program is verified before the program starts executing. Note that certain runtime errors (like division-by-zero) can still occur.
Algebra of types
Types in Numbat can be combined in various ways to produce new types. In its most general form, a type can be thought of as a product of physical (base) dimensions \( D_k \) with exponents \( \alpha_k \in \mathbb{Q} \): \[ \prod_k D_k^{\alpha_k} \] For example, the type Energy can be represented as Mass¹ × Length² × Time⁻².
Multiplication
This naturally allows us to multiply types (by combining the factors of both products into a single product).
We can use the *
operator to construct types for physical dimensions that are products of two or more (base) dimensions. For example:
dimension Time
dimension Current
dimension ElectricCharge = Current * Time
Exponentiation
We can also raise units to arbitrary powers \( n \in \mathbb{Q} \), by simply multiplying each \( \alpha_k \) with \( n \). The syntax uses the ^
exponentiation operator:
dimension Length
dimension Volume = Length^3
dimension Time
dimension Frequency = Time^(-1)
Division
Once we have multiplication and exponentiation, we can define the division of two types as
TypeA / TypeB ≡ TypeA * TypeB^(-1)
This is mostly for convenience. It allows us to write definitions like
dimension Power = Energy / Time
Note: When we talk about products of types in this section, we mean actual, literal products. Type theory also has the notion of product types which denote something else: compound types — like tuples or structs — that are built by combining two or more types. If we think of types in terms of the sets of all possible values that they represent, then product types represent the Cartesian product of those.
Type inference and type annotations
The type checker can infer the types of (most) expressions without explicitly declaring them. For example, the following definition does not mention any types:
let E_pot = 80 kg × 9.8 m/s² × 5 m
However, it is often helpful to specify the type anyway. This way, we can make sure that no mistakes were made:
let E_pot: Energy = 80 kg × 9.8 m/s² × 5 m
The type checker will compare the inferred type with the specified type and raise an error in case of inconsistency.
Function definitions also allow for type annotations, both for the parameters as well as the return type. The following example shows a function that takes a quantity of type Length
and returns a Pressure
:
let p0: Pressure = 101325 Pa
let t0: Temperature = 288.15 K
let gradient = 0.65 K / 100 m
fn air_pressure(height: Length) -> Pressure = p0 · (1 - gradient · height / t0)^5.255
Generic types
Numbat’s type system also supports generic types (type polymorphism). These can be used for functions that work regardless of the physical dimension of the argument(s). For example, the type signature of the absolute value function is given by
fn abs<D>(x: D) -> D
where the angle brackets after the function name introduce new type parameters (D
).
This can be read as: abs
takes an arbitrary physical quantity of dimension D
and returns a quantity of the same physical dimension D
.
As a more interesting example, we can look at the sqrt
function. Its type signature can be written as
fn sqrt<D>(x: D^2) -> D
Alternatively, it could also be specified as fn sqrt<D>(x: D) -> D^(1/2)
.
Limitations
The static type system also has some limitations. Let’s look at an exponentiation expression like
expr1 ^ expr2
where expr1
and expr2
are arbitrary expressions. In order for that expression
to properly type check, the type of expr2
must be Scalar
— something like
2^meter
does not make any sense. If the type of expr1
is also Scalar
,
everything is well and the type of the total expression is also Scalar
. An example
for this trivial case is an expression like e^(-x²/σ²)
. As long as the type
of x
is the same as the type of σ
, this is fine.
A more interesting case arises if expr1
is dimensionfull, as in meter^3
. Here,
things become difficult: in order to compute the type of the total expression
expr1 ^ expr2
, we need to know the value of expr2
. For the meter^3
example,
the answer is Length^3
. This seems straightforward. However, the syntax of the
language allows arbitrary expressions in the exponent. This is important to support
use cases like the above e^(-x²/σ²)
. But it poses a problem for the type checker.
In order to compute the type of expr1 ^ expr2
, we need to fully evaluate
expr2
at compile time. This is not going to work in general. Just think of a
hypothetical expression like meter^f()
where f()
could do anything. Maybe even
get some input from the user at runtime.
Numbat’s solution to this problem looks like this: If expr1
is not dimensionless,
we restrict expr2
to a small subset of allowed operations that can be fully
evaluated at compile time (similar to constexpr
expressions in C++, const
expressions in Rust, etc). Expressions like meter^(2 * (2 + 1) / 3)
are completely
fine and can be typechecked (Length^2
), but things like function calls are not
allowed and will lead to a compile time error.
To summarize: Given an exponentiation expression like expr1 ^ expr2
, the type checker
requires that:
expr2
is of typeScalar
- One of the following:
expr1
is also of typeScalar
expr2
can be evaluated at compile time and yields a rational number.
Remark
We would probably need to enter the world of dependent types if we wanted to fully
support exponentiation expressions without the limitations above. For example, consider
the function f(x, n) = x^n
. The return type of that function depends on the value
of the parameter n
.
Contact us
To contact us, either open a GitHub issue
or discussion, or pop into
#numbat
on Libera.Chat (link
to webchat).