Ka is a small calculator language for quick, day-to-day calculations.
Featuring...
- A GUI and CLI.
- Fractions:
(5/3) * 3
gives5
. - Units and unit conversion:
5 ft to m
. - Currencies and exchange rates:
5€ to $
. - Probability distributions and sampling, with a math-like syntax:
X = Bernoulli(0.3); P(X=1)
. - Plotting: comes with an ergonomic interface to Python's matplotlib.
- Arrays, also with math-like syntax:
{3*x : x in 1..3}
gives{3,6,9}
. - Lazy combinatorics:
10000000!/9999999!
gives10000000
rather than hanging like it would in other languages. - Dates and times:
(#2024-12-25# - now()) to days
gives the number of days until Christmas. - Intervals and interval arithmetic:
2*(1±0.1)
gives an interval from 1.8 to 2.2. - Other boring stuff: Variable assignment. Common math functions and constants.
Ka aims to be convenient: you can start the GUI with a keyboard shortcut, fire off a quick calculation, and close the GUI with Ctrl-W -- no mouse needed! Or if you're pottering about in the terminal, you can do a one-off calculation with ka '1+1'
.
More examples.
>>> 2 * (1/2)
1
>>> 1 metre + 1 foot to feet
4.28084
>>> p = 0.7; C(10,3) * p^3 * (1-p)^7
0.00900169
>>> p=.7; N=10; sum({C(N,k)*p^k*(1-p)^(N-k) : k in 0..4})
0.047349
>>> sin(90 deg)
1
>>> e^pi
23.1407
>>> X = Binomial(10, 0.3); P(3 <= X < 7)
0.6066
>>> line({0,1}, {0,1}, label: "hi", legend: true)
[...shows a line plot...]
Ka is currently distributed through the Python Package Index, see here.
To install Ka, first make sure you have the following prerequisites:
- Python 3.6+.
- Qt 5 (for the GUI). If your system uses the APT package manager, try:
apt install qt5-default
.
Then run this from the command line:
pip3 install ka-cli
You should now be able to run the ka
command -- see Usage below for various ways to use Ka.
- You'll need to run the installation command in an Administrator console.
- Following installation, an executable called 'ka.exe' should be created under the
Scripts
subdirectory of your Python installation. You may need to add the path to thisScripts
folder to the "PATH" environment variable (Advanced System Settings > Environment Variables). - If using pyenv, you probably need to run
pyenv rehash
so that pyenv can create a shim for the new executable.
There are various ways to interact with Ka: executing a single expression from the CLI; running an interpreter in the terminal; executing a script file; and a GUI.
To execute a single expression, pass it as an argument to the CLI. You may wish to surround the expression in single quotes so that it's not messed up by your terminal.
$ ka '1+1'
2
The CLI offers introspection commands to show the available units, functions and whatnot (run ka -h
for help with this).
Start the interpreter by executing ka
from your CLI with no arguments. There are interpreter-specific commands, prefixed by '%', like %help
. Run %help
to see a list of these commands.
$ ka
ka version 1.2
>>> 1+1
2
>>> %help
[...insert help text here...]
>>> quit()
$
Execute a script file using the --script
argument. Each statement must be separated by a semi-colon, and the value of the last statement will be printed to the console.
$ ka --script path/to/script.ka
To start the GUI, run ka --gui
.
Note: This manual includes the latest changes to the language, and won't necessarily correspond to the version you've installed. To find a "frozen" version of the manual, check out the branches of the repository, like 1.1.0
and 1.2.0
.
The basic unit of grammar is the statement. Multiple statements can be executed at once, separated by semi-colons:
>>> a = 3; b = 2; 2*a*b
12
An individual statement can be either an assignment (a = 3
) or an expression (1+1
).
An assignment consists of a variable name (such as a
), followed by =
, followed by an expression (such as 3
or 1+1
or sin(90 deg)
). Assignments are not expressions, so you can't nest assignments like a=(b=3)
. You can, however, assign the value of one variable to another: a=3; b=a;
.
An expression is a sequence of math operations that returns a value: addition, subtraction, function calls, and so on. If the value of an expression is a quantity (a number with a unit attached), then the unit can be converted to something else using the operator to
. For example, this assigns a
the magnitude of 3 metres when it's converted to feet: a = 3m to ft
.
pi
and e
are the only mathematical constants provided. They're treated like variables and can be overwritten: pi=3
, woops. true
and false
are also variables, with the values 1
and 0
, respectively.
Numbers can be integers (123
), floats (-1.23
), fractions (1/3
), and they can be provided in scientific notation (1.23e-7
).
The typical selection of number bases are supported: use the 0b
prefix for binary, 0o
for octal, and 0x
for hexadecimal. So 0x10
is 16 in base-10. Note that numbers with alternative bases can only be integers, and can't be mixed with scientific notation (otherwise, there's a parsing ambiguity in something like 0x1e-10
).
Ka is strongly typed, not statically typed. This means that when you pass a fractional number to a function that expects an integer, the type system will complain. But you don't have to declare the type of anything in advance.
The type system consists of (1) a hierarchy of numerical types, (2) quantities, and (3) some other types like arrays and random variables that don't mix with the other types so much.
The hierarchy of numerical types goes: Number
> Real
> Rational
(Fraction) > Integral
(Integer). There's also a Combinatoric
type, used to lazily evaluate combinatoric operators/functions like !
and C
, and Number
> Combinatoric
.
'Real' numbers are represented as floating point numbers. If a fraction can be simplified to an integer, such as 2/2, then this will happen automatically. In the other direction, a type that is lower down the hierarchy, such as an integer, can be cast into a type that's further up the hierarchy in order to match a function signature.
Bool
(stands for Boolean) is essentially an alias for the Number
type. Non-zero values represent true. The variables true
and false
are provided for convenience, but they're just proxies for the values 1 and 0.
Quantities consist of two components: a magnitude and a unit (see: the section on units). Any quantities can be multiplied together or divided into each other, but only quantities of the same unit type can be added or subtracted. For example, you can add 1 metre
and 1 foot
, but not 1 metre
and 1 second
. This is enforced by the binary operators themselves (addition and subtraction).
Most arithmetic functions can be applied to both Numbers and Quantities.
String
s, like "hello world"
, are (so far) only used as configuration parameters for the plotting interface, and there's no way to manipulate or combine them.
Other types like Instant
s, Interval
s and Array
s are discussed in later sections.
Ka has functions and 3 types of operators: binary operators, prefix operators, and postfix operators.
The binary operators, like addition (+
), exponentiation (^
) and division (/
), take two arguments and come between those arguments, like 1+1
.
The prefix operators are +
and -
. They take a single argument and come before that argument: +1
and -1
.
The only postfix operator is !
(factorial), which takes a single argument and comes after that argument: 9!
.
Operator precedence goes:
!
^
*
,/
,%
+
,-
<
,<=
,>
,>=
,==
,!=
This means that 2^3!*5+1
gets parsed the same as ((2^(3!))*5)+1
.
Functions accept positional arguments and keyword arguments. A function call can look something like the following: f(x, y, keyword_arg: 1, another: "hi")
. Some functions, like plot
, accept a variable number of the same argument type; plot
happens to accept any number of Plot
-type arguments.
Here's a selection of functions and operators in the language. To list all the functions, run ka --functions
. To find out more about any particular function (including what types of arguments it accepts), run the CLI command ka --function {name}
, or run the interpreter commands %f {name}
or %fun {name}
.
- +, -, *, /, %, ^, <, <=, ==, !=, >, >=, sin, cos, tan, sqrt, ln, log10, log2, abs, floor, ceil, round, int, float, log, C, !, quit
Here are most of the units supported by the language. To see a complete list (excluding currencies), run ka --units
from the command-line.
- second (s), metre (m), gram (g), ampere (A), kelvin (K), mole (mol), candela (cd), hertz (Hz), radian (rad), steradian (sr), newton (N), pascal (Pa), joule (J), watt (W), coulomb (C), volt (V), farad (F), ohm (ohm), siemens (S), weber (Wb), tesla (T), henry (H), degC (degC), lumen (lm), lux (lx), becquerel (Bq), gray (Gy), sievert (Sv), katal (kat), minute (min), hour (h), day (d), astronomicalunit (au), degree (deg), hectare (ha), acre (acre), litre (l), tonne (t), dalton (Da), electronvolt (eV), lightyear (lj), parsec (pc), inch (in), foot (ft), yard (yd), mile (mi), nauticalmile (sm), teaspoon (tsp), tablespoon (tbsp), fluidounce (floz), cup (cup), gill (gill), pint (pt), quart (qt), gallon (gal), grain (gr), dram (dr), ounce (oz), pound (lb), horsepower (hp), bar (bar), calorie (cal)
The following prefixes are also supported, mostly coming from the SI standard. For convenience, their shorthand names and multipliers are provided here.
- yotta (Y, 10^24), zetta (Z, 10^21), exa (E, 10^18), peta (P, 10^15), tera (T, 10^12), giga (G, 10^9), mega (M, 10^6), kilo (k/K, 10^3), hecto (h, 10^2), deca (da, 10^1), deci (d, 10^-1), centi (c, 10^-2), milli (m, 10^-3), micro (μ, 10^-6), nano (n, 10^-9), pico (p, 10^-12), femto (f, 10^-15), atto (a, 10^-18), zepto (z, 10^-21), yocto (y, 10^-24), kibi (Ki, 2^10), mebi (Mi, 2^20), gibi (Gi, 2^30), tebi (Ti, 2^40)
Notes on units:
- Division in the unit signature is represented by the symbol
|
, so 1 metre per second is written1 m|s
. This avoids parsing ambiguities. A more complex unit signature is1 kg | m s^2
, which is the same as1 pascal
. - To find out more about a specific unit, run
ka --unit {name}
, or execute%u {name}
or%unit {name}
in the interpreter. - Units are case sensitive.
- You can convert from one unit to another using the
to
operator:1m to feet
. - Units are part of what makes up a quantity, together with a magnitude. It only makes sense to add or subtract quantities of the same unit type. You can add two areas, for example, but it doesn't make sense to add an area and a velocity. You can multiply and divide any quantities, however.
- A unit can be a multiple of base units (a pound is 0.45 kilograms), but it can also have an offset, as in the case of the degree Celcius, which is offset from the kelvin by -273.15. This makes degC tricky to work with and as a result you can't generally combine it with other units.
- UK / Imperial measures are used for the teaspoon and other ambiguous (mostly cooking-related) units, see: https://en.wikipedia.org/wiki/Cooking_weights_and_measures
As for how the unit system works, it's based on the International System of Units (SI). All units are represented in terms of the 7 SI base units: second, metre, gram, ampere, kelvin, mole and candela. (Update -- see the next section for a newly-added base unit: cash). Feet are a multiple of the metre, and their "signature" in base units is m^1
. Frequency, measured in hertz, is s^-1
. Area is m^2
. Velocity is m s^-1
. Internally, the "unit signature" of a quantity is a 7-dimensional vector of integers, with each dimension corresponding to one of the SI base units. For example, 1 metre may have a unit signature of (1, 0, 0, 0, 0, 0, 0). 1 metre per second may have a unit signature of (1, -1, 0, 0, 0, 0, 0, 0). When you multiply two quantities together, their unit signatures are added together. When you divide, the unit signature of the divisor is subtracted.
Further reading for the interested:
- https://en.wikipedia.org/wiki/Quantity
- https://en.wikipedia.org/wiki/International_System_of_Units
- https://en.wikipedia.org/wiki/Dimensional_analysis
- https://www.hillelwayne.com/post/frink/
- https://gmpreussner.com/research/dimensional-analysis-in-programming-languages
The unit system also incorporates currencies. They exist in an 8th dimension: the dimension of cold, hard cash. Currencies can be referenced using their ISO-4217 code names (like eur and gbp). Special symbols are provided for some currencies: € for eur, $ for usd, $ for gbp, and ¥ for jpy. Currencies can also be referenced by longer names (e.g. "mexicanpeso"), but these are verbose and subject to change.
A pre-scraped database of currencies and exchange rates are shipped with Ka, which are current as of December 23rd, 2024. To re-scrape this data, run ka --scrape-currency-to /path/to/file
, and then move the resulting file to ~/.config/ka/currency
(or, on Windows, %userprofile%\AppData\Local\ka\currency
) - at least, that's the default path, but you can change it with the currency-path
configuration parameter.
Another configuration parameter is base-currency
, which is set to eur
(the ISO-4217 code name of the Euro) by default. All cash amounts are represented in the base currency, so 1 usd
will automatically be converted to 0.961345 eur
(or whatever).
The introspection commands of the interpreter/CLI do not display currencies alongside the other units, since there are too many currencies. Instead, use ka --currencies
, or, in the interpreter, %cs
/ %currencies
. This will show you the exchange rates as well as the currency names and symbols.
The following examples show that: 1. if you worked 24/7 for 2000 years, earning $1000 per hour, you still wouldn't be the richest person in the world; 2. if you happened to find a USB stick containing 100 bitcoins, you'd be a multi-millionaire; and 3. if you dropped 1 million dollars in 1-dollar bills on your head, you'd be hit with a force of 9810 newtons. Another reason not to hoard wealth.
>>> 1000 dollars|hour * 2000 years to billion dollars
17.52
>>> 100 bitcoin to million euro
8.9007
>>> mass_per_dollar = 0.001 kg | dollar
>>> G = 9.81 m|s^2
>>> 1 million dollars * mass_per_dollar * G to newtons
9810
First, the usual utilities for randomness:
rand()
gives a random number in the range 0-1.seed(n)
sets the (integer) seed for random number generation and sampling.
Additionally, a number of discrete and continuous probability distributions / random variables are provided. Various properties of these distributions can be calculated, and they can be sampled from. A full list of distributions is shown below. For now, let's say we've already entered X = Binomial(10, .5)
. Then:
E(X)
ormean(X)
gives the expectation, a.k.a. the mean.P(X=3)
gives the probability of the value 3 (discrete random variables only).P(X<3)
,P(1 < X <= 3)
,P(X > 5)
calculate the probability of a range.sample(X)
returns a random value from the distribution.sample(X, n)
returnsn
random values from the distribution.
These are the discrete probability distributions and their parameters:
Binomial(n, p)
:n
trials and success probabilityp
.Poisson(lambda)
: ratelambda
(an integer, representing the average).Geometric(p)
: success probabilityp
.Bernoulli(p)
: success probabilityp
.UniformInt(lo, hi)
: uniform distribution over the integerslo
,lo+1
, ...,hi-1
,hi
.
And these are the continuous ones:
Exponential(lambda)
: ratelambda
.Uniform(lo, hi)
: uniform distribution over real numbers betweenlo
andhi
.Gaussian(mu, stddev)
: normal distribution with meanmu
and standard deviationstddev
.
Here's an example (found at examples/estimate-pi.ka
; run with ka --script examples/estimate-pi.ka
) that samples from the Uniform
distribution to estimate the value of pi
. It makes use of the Array type, discussed in the next section.
N = 10000;
X = Uniform(0, 1);
xs = sample(X, N);
ys = sample(X, N);
distances = {sqrt(x^2+y^2) : x in xs, y in ys};
f = size({d : d in distances, d<=1})/N;
4*f
Arrays are written like so: {1,2,3}
. They're basically a shim over Python lists.
The elements can be arbitrary expressions: {1+1,2*x, 1 m}
.
The special synax lo..hi
generates a range of integers lo
, lo+1
, ..., hi
.
Based on the mathematical notation for sets, arrays can also be generated from a series of clauses / conditions. For example, to calculate the sum of the squares of all odd numbers between 1 and 10: sum({x^2 : x in 1..10, (x%2)==1})
.
Array-related functions, given an array A
:
sum(A)
calculates the sum of the elements.prod(A)
calculates the product of the elements.mean(A)
calculates the mean.median(A)
calculates the median.size(A)
returns the number of elements in the array.max(A)
returns the maximum element in the array.min(A)
-- need I say more?range(lo,hi)
returns an array of all integers between the integerslo
andhi
(bounds are inclusive).lo..hi
is syntax sugar for calling this function.range(lo,hi,step)
returns numbers betweenlo
andhi
in steps of sizestep
.x in A
returns whetherx
is in the ArrayA
.
The Instant
type represents a particular moment in time. An instance of this type can be created using the syntax #1984-01-25#
, where any ISO-8601-formatted string can be substituted between the "#" delimiter.
The following functions and operations are also available for working with time:
now()
gives the current date & time in the local timezone.today()
gives the current date in the local timezone, with the time set to 00:00:00 (midnight).floor(instant)
returns a copy of the Instant with the time set to midnight at the START of the day.ceil(instant)
returns a copy of the Instant with the time set to midnight at the END of the day.- Time quantities (like
10 seconds
) can be added to an Instant to get a new Instant. Same for integers, in which case the integer represents a number of days. You can also subtract time quantities and integers from an Instant. - Subtract two instants to get the time between them.
Instant
s can be compared using the usual operators:==
,!=
,<
,>=
, ...year(I)
,month(I)
,day(I)
,hour(I)
,minute(I)
,second(I)
extract the different components of an Instant.
The following examples show how to calculate: 1. the number of days until Christmas, 2. the number of hours until 9am tomorrow, and 3. the number of seconds until 16:21:10, March 8th, 2025.
>>> (#2024-12-25# - now()) to days
5
>>> (today() + 1 day + 9 hours) - now() to hours
10.8956
>>> #2025-03-08T16:21:10#-now()
6718341.204931 s
The following interface is basically a shim over Python's matplotlib plotting library. The drawing functions, like line(...)
and histogram(...)
, return a Plot
, which can then be passed to the plot(...)
function in order to render it. Alternatively, if a Plot
is the last value in a script, or is returned at the REPL, it will be rendered implicitly. Wherever a colour
parameter is expected (as a String) (yes, British English spelling, sorry), it should follow the format expected by the matplotlib API ("red", "#0f0f0f", ...). The same applies for other String-type arguments that get passed along to matplotlib.
Here's an example. Executing this script (ka --script examples/trigplot.ka
) will render a plot of sin(x) versus cos(x).
xs = {0.2*i : i in 0..100};
plot(
options(
integer_x_ticks: true,
xlabel: "x",
ylabel: "y",
grid: true,
legend: true),
line(xs, {sin(x) : x in xs}, label: "sin(x)", colour: "blue"),
line(xs, {cos(x) : x in xs}, label: "cos(x)", colour: "red"));