By dkl9, written 2022-231, revised 2023-230 (3 revisions)
RTensor is the sixth-best formula calculator in the world. (I wrote that slogan a long while ago. Don't ask what the five better ones are.)
All valid code in the RTensor language gets parsed into an abstract syntax tree (AST). The AST consists of nodes, which may or may not reference subnodes, making them "branch nodes" (as e.g. operations and function calls) and "leaf nodes" (as primitives) respectively.
ASTs are common to many languages, and you don't really need to know about them for basic RTensor use.
Consider the following progression:
All items in that progression qualify as tensors of various ranks; ranks of tensors continue beyond 2 as far as you want, tho ranks from 0 to 2 occur most commonly.
The most developed interface for RTensor is the webpage, which consists of some documentative content, followed by a space for alternating lines of input and output.
To run a line of RTensor code, type it (or copy it) into the current input line and press Enter
.
You can also use the up and down arrow keys to navigate thru the current session's history of earlier lines.
Whilst not in the RTensor language, you can combine multiple lines of code into one input by joining them with semicolons (;
).
When no error occurs whilst processing the input code, RTensor will write as output the code's return value. Preceding this return value, RTensor indicates a variable name (an underscore followed by a serial number) to which it saved that return value. If the input code consists of multiple semicolon-separated statements, RTensor will evaluate all of the statements, but will only present the last one's return value, and the serial variable will only receive the final statement's return value.
RTensor also silently saves the final return value to the variable _
(underscore), but leaves its previous value if an error occurred.
Some code (such as syntactic functions) produces extra output, in varying forms, before the main output.
When an error occurs whilst parsing or evaluating the input code, RTensor will display a relevant error message.
If the error starts with a PascalCase
error type, that means it came directly from JavaScript, and should be regarded as a bug.
If the error occurs when evaluating, RTensor will not execute code after the part that triggered the error; errors propagate up thru a program.
For some code (such as 3+
), RTensor successfully parses and evaluates it, but notices that not all of the code corresponds to part of a valid AST.
In addition to typical output, RTensor leaves the message "possible syntax error", which almost always means "actual syntax error".
A numeric literal directly represents a real number (a scalar), and consists of one or both of an integer component and a fractional component.
Each component consists of digits (in base ten).
The fractional component, if present, must begin with a dot (.
).
Valid numeric literals include 0
, 389
, 3.14159
, and .367
.
For now, you can use .
to mean NaN
, but this may change.
An identifier consists of a sequence of letters, digits, underscores, or (unlike most other languages) apostrophes (akin to prime symbols), and does not start with a digit. An identifier, when interpreted to evaluating code, references a variable, which can hold a value of any type. Shadowing/function scope may affect what variable an identifier references.
Operators combine one or two subexpressions (making the operation unary or binary, respectively) and describe a computation based on the subexpressions. Unary operators go before their subexpression. Binary operators go between their subexpressions.
Each operator has a precedence, which may group it more tightly or loosely than other operators, and each precedence has an associativity. Unary operators all have tighter precedence than binary operators, regardless of their relative precedence as binary operators. You can use parentheses to group operations differently from what their precedence and associativity would normally imply.
In operator descriptions, a
and b
represent subexpressions and their results.
When used on a scalar and a tensor, the result comes from the tensor, with each element replaced by the result of the operation between the scalar and each scalar in the tensor.
For example, 3 * ((7, 2), (-1, 3), (-8, -2))
returns [[21, 6], [-3, 9], [-24, -6]]
.
When used on two tensors, the result comes from the elementwise combination of the two tensors, or an error if the tensors have different structures.
a + b
, or +a
(identity)a - b
, or -a
(negative)a * b
a / b
, or /a
(reciprocal)a ^ b
, or ^a
(exp(a) = ea)a b
(base-a logarithm of b), or a
(natural logarithm)a == b
(true or false), or == a
(equal to zero)a -/ b
(ath root of b), or -/a
(square root)a < b
(true or false), or < a
(negative)
You cannot chain comparisons, as in a < b < c
;
RTensor will interpret that as (a < b) < c
, which reduces to either true < c
or false < c
.
RTensor has no >
operator.
Instead, swap operands as appropriate.a, b
gives an output which depends on the relative rank of a
and b
:
a
has greater rank, RTensor appends b
to the end of a
, returning a new tensora
and b
as its elementsThese semantics let you chain the appending operator (as in a, b, c, d
) to build a many-element tensor.
a .. b
gives an output which depends on the ranks of a
and b
:
b
to the end of a
, returning a new tensora
and b
are scalars), RTensor generates a vector by iterating thru the numbers from a
to b
(inclusive) with a step size of 1a .. b
is an errorWhen using the concatenation operator, carefully distinguish it from a decimal point;
e.g. 3..5
will result in an error, unlike the valid options 3.5
and 3 .. 5
.
.. a
encapsulates a
as a one-element tensor, effectively incrementing the rank without adding any new data.
a => b
constructs and returns an anonymous function.
a
may consist of a comma-separated list, each of which labels a separate argument for the function;
it gets tranlated into a pattern, but syntactically forms as an expression.
b
must consist of some expression, possibly using variables defined in a
.
For example, x, y => x + y
makes addition into a two-input function.
The assignment operation, a = b
, saves the value from b
into the location described by a
, and returns the new value of a
(or something closely related to a
, by undocumented semantics).
a
may consist of an identifier — in which case, b
replaces the value already stored in that variable.
a
may have a function call or index suffix (the two have the same form):
a
already references a function, the assignment adds a new case (the pattern consisting of the argument list, the expression coming from b
) to that function (at top priority)a
already references a tensor, the assignment replaces the element at the given index with the value of b
, or causes an error if a
does not already contain an element at that indexa
does not reference anything, or references a scalar, the assignment replaces the value with a new function, adding a single case as if a
already referenced a functionFrom tightest to loosest, L = left-associative, R = right-associative:
An expression suffixed with a parenthesised expression (v(i)
), or the equivalent bracketed form (v[i]
), forms an indexing expression (or a function call, depending on v
);
if v
refers to a tensor, the index expression returns the i
th element of v
(a tensor of rank at most one less than that of v
).
Indexing in RTensor starts at 1 and not 0.
Attempts to access v(i)
where i
exceeds the length of v
, i
does not equal an integer, or i
subceeds 1 will cause an error.
The i
component of the indexing expression can include extra comma-separated indices, but RTensor currently ignores all indices after the first.
An expression suffixed with a parenthesised expression-list (f(a, b, c, ...)
) forms a function call (or an indexing expression, depending on f
);
if f
refers to a function, the function call returns the value f
provides when evaluated with the given argument list.
The argument list may contain any number of arguments, including 0 (i.e. f()
).
Whilst ,
normally acts as an operator, the argument-list syntax suppresses this behaviour.
In an argument list, ,
acts as an operator iff it occurs in parentheses, as in the first argument of f((a, b), c)
.
Operators with precedence looser than that of ,
(i.e. =>
and =
) will not work at all unless parenthesised.
When evaluating a function, RTensor first evaluates all the arguments, then searches for a pattern (going in descending priority) that matches the arguments. If RTensor could not find a matching pattern, an error occurs. Upon finding a matching pattern, RTensor evaluates the expression or internal function corresponding to that pattern in the function's definition, using the argument-name correspondences given from the pattern to make local variables for that expression.
If a function is called with bracket syntax (f[a, b, c, ...]
) rather than the parenthetical syntax, the arguments will be given to the function as expressions (syntax), not values;
they will be left unevaluated.
Whether this is useful depends on the function.
Typically, a function is only usable with either evaluated arguments (the more common case) or syntactic arguments.
Functions, as objects in RTensor (or function definitions), consist of a list (arranged by priority) of pattern-expression pairs, or cases. Some functions (those predefined by the code which sets up RTensor) have, in place of an expression defined from RTensor, a function in the language implementing RTensor.
A pattern consists of a list of zero or more parameters. A parameter consists of either an identifier or some other expression:
When checking if a pattern matches a list of arguments, the arguments get aligned to the parameters; if the number of arguments differs from the number of parameters, the pattern does not match without any further checks.
To make a parameter that matches only values which equal an existing variable, wrap the variable identifier in an identity (such as x + 0
or x * 1
).
RTensor currently does not provide pattern-matching mechanisms more complex than equality checks against constant values; you must do so yourself with explicit checks.
RTensor provides both an assignment-based syntax and an anonymous maplet-based syntax for defining functions. The former works with (named) variables, but the latter constructs a function without saving it to a variable (anonymous). A maplet operation takes its left operand as a pattern and its right operand as an unevaluated sub-expression, constructing and returning a single-case function, usable in the same contexts as any other function.
In that single case's expression, one can use variables defined in the scope which defines the anonymous function, creating a sort of closure.
If the left operand has commas, RTensor will break it down into a list of patterns, allowing for more than one parameter, rather than a single pattern.
Using closures, one can make multi-argument anonymous functions a different way:
make a function that returns another function, as a partially-evaluated form of the desired multi-argument function — for example, x => (y => x + y)
instead of x, y => x + y
.
RTensor does not have a special syntax to facilitate the requisite chained method calls, so one would use the curried version not as f(a, b)
but f(a)(b)
.
In descriptions of functions, identifiers in parentheses or brackets (as appropriate) correspond to arguments for valid ways to call a function. The exact identifier used indicates the expected type:
a
, b
, c
: number (in some contexts, specifically integer)f
, g
: functionk
, m
, n
: integert
: tensor (any rank above 0)v
: vector (rank 1 tensor)x
, y
: number (rank 0 tensor)w
, z
: any typeSome functions work on a tensor, but only look one rank deep, using the tensor in place of a vector.
In this case, the parameter uses t
for the identifier.
All functions in this list that accept a real number (so not random
) will accept, in place of that number, a tensor, and will perform the same operation on each number in the tensor, returning an equivalently-structured tensor.
abs(x)
, the absolute value or magnitude of x
floor(x)
, the integer rounded down (towards negative infinity) from x
random()
, a random number in [0, 1)
, or random(m, n)
, a random integer in [m, n]
sin(x)
cos(x)
arcsin(y)
arccos(y)
arctan(y)
, or arctan(y, x)
re(x)
, the real component of complex number x
im(x)
, the imaginary component of complex number x
conj(x)
, the complex conjugate of x
zero(n)
, an n
-element vector of zeroeslen(t)
, the number of elements in the top level of t
This does not count the number of numbers in t
, but merely the number of tensors of lesser rank immediately composing t
.
For example, len(((4, 7), (2, 3), (8, 5)))
return 3 rather than 6.any(v)
, the first nonzero value in v
.
If v
consists of true
/false
values, this functions as an inclusive OR.all(v)
, the first zero-equivalent value in v
.
If v
consists of true
/false
values, this functions as an AND.map(t, f)
, a new tensor (same rank as t
, greater than 0) with each element x
from t
(in the top level) replaced with f(x)
.
If f
accepts two arguments (binary function), map
will replace x
with f(x, i)
, where i
matches the index of x
from t
.
If f
does not accept any arguments (nullary function), map
will replace x
with f()
.
Notice that map
only acts on top-level elements;
e.g. map(((4, 7), (2, 3), (8, 5)), f)
applies f
to (4, 7)
, (2, 3)
, and (8, 5)
, not 4
, 7
, 2
, 3
, 8
, 5
.
To apply a function to replace every number in a tensor, use deepmap
.filter(t, f)
, a new tensor (same rank as t
, greater than 0) containing all elements x
from t
(in the top level) for which f(x)
returns a value equivalent to true
.
If f
accepts two arguments (binary function), filter
will copy x
for which f(x, i)
returns a value equivalent to true
, where i
matches the index of x
from t
.
As with map
, filter
only acts on top-level elements.
RTensor currently does not have any deepfilter
to check f(x)
for every number in a tensor.reduce(t, f)
, a value computed by repeatedly applying x = f(x, y)
for each element y
of t
(at the top level).
The initial value of x
comes from the first element of t
;
reduce
does not apply f(x, y)
using that initial value for y
.
As with map
, reduce
only acts on top-level elements.
RTensor currently does not have any deepreduce
to combine all individual numbers in a tensor.tail(t)
, a new tensor consisting of all the elements of t
, in the same order, except for the firsttrim(t)
, a new tensor consisting of all the elements of t
, in the same order, except for the lastdeepmap(t, f)
, a new tensor with the same structure as t
, but with every number x
(down thru all ranks) replaced with f(x)
if(c, w, z)
or if[c, w, z]
, a selection between w
and z
depending on c
, selecting w
iff c
equates to true
.
if
accepts up to two extra arguments to automatically apply to the selected value as arguments (assuming that value has callable behaviour).
For more than two arguments (and recommended in general, as we may deprecate that feature), we suggest directly calling the result instead, as in if(c, w, z)(a, b, x, y)
.for(init, cond, step)
, the final value of x
— which starts as x = init
— after repeatedly applying x = step(x)
, until cond(x)
returns a value equivalent to false
clear()
, which clears display but not history or variableslhs[expr]
, the left side of expr
rhs[expr]
, the right side of expr
operator[expr]
, a number corresponding to the type of operator in expr
sum[(var = expr), n, expr]
, corresponding to typical summation notationproduct[(var = expr), n, expr]
, likewiserender[expr]
, which (crudely) renders the expression in the webpage versionhtml[expr]
, which outputs HTML code to display the expressionlatex[expr]
, which outputs LATEX code to display the expressioni
, the imaginary unitpi
phi
, the golden ratio 1.618true
false
null
If you want e
, use ^1
.