VeLa - AAVSO/VStar GitHub Wiki
- Introduction
- Types
- Sequences
- Expressions
- Operators
- Bindings
- Functions
- Names
- Selection
- Iteration
- Grammar
- Contextual examples
- Running VeLa code
- Intrinsic named constants
- Intrinsic functions
VeLa (VStar expression Language) is a general purpose programming language that, within VStar, can be used to:
- specify a numeric expression wherever a number can be used as input;
- create complex observation filter and search expressions;
- specify model functions;
- transform observations.
For examples of how VeLa is used in VStar, see Contextual examples.
VeLa is a simple functional language with:
- integer, real, string, boolean, list, function types
- variables that take on the type of the value to which they are first bound via the
<-
(assignment) operator (e.g.n <- 1
) - named constants via the is operator (e.g.
π is 3.1415926
) - case insensitive, Unicode keyword, variable, and function names
- statically typed, recursive, higher order, named or anonymous functions that may be overloaded by parameter type
- selection (
when
) and iteration (while
) - numeric operators:
+ - * / ^
- string operators:
+
in
- logical operators:
not
,and
,or
- relational operators:
= <> > < >= <=
- list operators:
in
- regular expressions (Java syntax, see Summary of regular-expression constructs) for use with
=~
(approximately equal) operator - collection of useful intrinsic functions (see list at end)
VeLa is an interpreted, dynamically typed, domain specific language primarily intended for use within VStar.
Some other languages are too verbose or too weakly typed for the intended purpose or for the developer's taste. A disadvantage of VeLa is that it is not an existing, well-known language. An advantage is control over evolution, implementation, and integration with VStar.
VeLa code is translated from text into a more efficient internal representation before execution.
This document corresponds to the VeLa language for the latest official release of VStar.
VeLa supports values of the following types, with examples:
- integer, e.g.
42
,-42
- Integer values are signed 64-bit (long) numbers
- real, e.g.
4.2
,0.42e1
- Note that localised real numbers can be used in VeLa, so that if the decimal point in the current locale is a comma, this can be used instead of
.
, e.g.4,2
vs4.2
.
- Note that localised real numbers can be used in VeLa, so that if the decimal point in the current locale is a comma, this can be used instead of
- boolean,
true
orfalse
- string, e.g.
"42"
,"(∃x)P(x)"
- function
- e.g. two anonymous (unnamed) function expressions that when applied to
3
yield3^3
or27
:-
function(n:integer) : integer { n^3 } (3)
, or withλ
orΛ
: λ(n:integer) : integer { n^3 } (3))
-
- see also the Functions section below
- e.g. two anonymous (unnamed) function expressions that when applied to
- list
- e.g.
[41 42 43]
,[1 2 "many"]
,[2 3 5 λ(x:integer):integer{x*x}]
,[1 "✊" "✋" "✅" "❓" 6]
- note the absence of commas between list elements
- lists are heterogenous, i.e. each element may be of any type
- e.g.
There is also a special type called any
which is a placeholder for one or more values of any type. Its use is restricted to a small number of functions (help
, print
, println
at time of writing) which can take one or more values of any type. See also https://github.com/AAVSO/VStar/issues/199. A small number of intrinsic functions also take parameters or return values of type object
. Neither object
nor any
should be considered to be first class types in the language, neither may currently be used explicitly in VeLa code, and one or both may be deprecated in the future.
A VeLa program is sequence of expressions and named bindings (variable, constant, function).
VeLa expressions consist of values of these types combined in various ways using operators, functions, selection, and iteration constructs.
- integer and real number operators:
+
-
*
/
^
- The first four correspond to the familiar ones: addition, subtraction, multiplication, division
- The last is exponentiation, e.g.
2^3
which is8
.
- string operators:
+
in
- An example of
in
is:"42" in "the meaning of life is 42"
, which will returntrue
- An example of
- logical operators:
not
,and
,or
- relational operators:
= <> > < >= <=
- list operators:
in
+
-
*
/
^
- An example of
in
is:"42" in "the_meaning_of_life is 42"
, which will returntrue
. -
+
-
*
/
^
can be applied to lists or to a scalar and a list, e.g.-
[1 2 3] + [4 5 6]
gives[5 7 9]
-
2 * [4 5 6]
or[4 5 6] * 2
gives[8 10 12]
-
"a " + ["cube" "sphere" "cylinder"]
gives["a cube" "a sphere" "a cylinder"]
-
- An example of
- approximately equal to:
=~
- for use with regular expressions (Java syntax, see Summary of regular-expression constructs)
- e.g.
"12abc45" =~ "^\d+\w+\d+$"
will returntrue
A named function starts with an identifier, followed by zero or more typed parameters, between parentheses, separated by spaces, and then an optional return type. The body of the function comes next. The function can be preceded by a help comment that can later be accessed via the help
function, e.g. help(find)
. The last expression computed is returned as the value of a function.
This dry, abstract description is best illustrated by an example:
cube(n:real) : real {
n*n*n
}
This code defines a function called cube
that takes a real number, computes its cube, and returns it.
Functions must specify the types of their parameters and if a value is to be returned, the return type must also be specified.
Spaces, not commas, separate multiple parameters, e.g.
apply_f_to_list(f:function xs:list) : list {
map(f xs)
}
The VeLa interpreter checks the types of variables and functions at run-time. Unlike some dynamically typed languages, a function will not run at all if the types of its parameter list (so-called "formal parameters") are not compatible with the actual parameters passed to it.
Functions may be overloaded, having the same name but different parameter or return types, e.g.
f(n:integer):integer{
n^4
}
f(n:real):real{
n^4
}
If called with an integer parameter, the first function will be invoked. If called with a real parameter, the second function will be invoked, e.g. f(2.0)
will return 16
as will f(2)
. The difference in the value returned can be seen with the help
function, e.g. help(f(2.0)
:
REAL : 16
If the actual parameter does not match any known form of the function, e.g. f("2")
, an error such as this will follow:
Invalid parameters for function "F":
F(N:INTEGER) : INTEGER
F(N:REAL) : REAL
Their higher-order nature means that functions are also expressions, so can be bound to variables or named constants, passed as function parameters, and returned from functions, i.e. they are higher-order functions (HOFs).
This example defines a multiplication function which is used by the function n!
to compute factorial by reducing a sequence from 1
to n
:
mul(n:integer m:integer) : integer {n*m}
n!(n:integer) : integer { reduce(mul seq(1 n 1) 1) }
e.g.
n!(10)
3628800
An anonymous function can be introduced with the function
keyword or the λ
(lambda) symbol, e.g.
find(λ(n:integer):boolean{n > 10} [4 6 10 21 8 42])
This returns 3
, the index of 21
in the list to which the find
function applies an anonymous function that takes an integer and returns true if that number is greater is 10
, terminating the execution of find
.
Variable, constant, function, and parameter names must start with an underscore, letter, or Unicode character in the range hexadecimal 80 to FFFF, and can be followed by multiple letters, underscores, digits, symbols (? ! & % # $
) or Unicode characters in the range hexadecimal 80 to FFFF. Unicode characters can be copied and pasted into VeLa code, as shown in this and other sections.
For example, here is a named constant called "⭕":
⭕ is "red circle (i.e. ⭕)"
A binding creates a variable, e.g.
x <- 42
or a named constant:
x is 42
that takes on the type of the first value it is bound to. In these examples, the type is integer
.
A named constant binding cannot be changed. The type of a variable binding cannot be changed and should be preferred unless variability is actually required.
if
and when
constructs allow choices to be made, different branches of code to be taken or expressions evaluated.
The form of an if
expression is if
boolean-expression then
consequent1 else
consequent2 with else
consequent2 being optional. If boolean-expression is true
, consequent1 is the result of the whole if
expression, otherwise consequent2 results.
Below is an example of its use in a function (recursive factorial: n!
).
n!(n:integer) : integer {
if n = 0 then 1 else n*n!(n-1)
}
If the consequent is a binding instead of an expression, no result is returned from an if
expression, and if
is treated as a statement.
The when
statement is a selection construct that checks a series of Boolean expressions one by one, returning the value of the first matching expression. The form of a when
expression is when
boolean-expression1 -> consequent1 boolean-expression2 -> consequent2 .. boolean-expressionN -> consequentN).
Here is an example of a function that takes a real value, t
, and uses a when
expression to return the value of an expression of the form mt + b
(a linear equation).
f(t:real) : real {
when
t < 2451482.87956 -> -0.056438243321*t + 138360.8422308304
t >= 2451482.87956 and t < 2451499.19244 -> -0.007211142945*t + 17681.44844775249
t >= 2451499.19244 and t < 2451519.71482 -> 0.008330270959*t + -20418.315186923635
t >= 2451519.71482 and t < 2451541.82028 -> 0.027192077131*t + -66658.4048747983
true -> 0.038448083037*t + -94252.97408292542
}
This is similar to the cond
(conditional) construct in the LISP language.
Loops in VeLa are often unnecessary due to functions that operation on lists such as find
, map
, for
, but where general purpose iteration is required, the while
loop can be used.
The form of a while
expression is while
boolean-expression body. Here is an example:
i <- 1
while i <= 10 {
println(format("%d^3 = %d" [i i^3]))
i <- i + 1
}
Running this gives:
1^3 = 1
2^3 = 8
3^3 = 27
4^3 = 64
5^3 = 125
6^3 = 216
7^3 = 343
8^3 = 512
9^3 = 729
10^3 = 1000
The same effect could be achieved via the for
procedure which applies a function (procedure in this case since nothing is returned) to each element of a list, in this case the integer sequence 1..10. The advantage here is that there is no variable increment necessary.
for(
λ(i:integer){
println(format("%d^3 = %d" [i i^3]))
}
seq(1 10 1)
)
If a value is returned from the function passed to for
, the last value returned will be the final output of the for
function, e.g.
sum <- 0
for(
λ(i:integer):integer{
sum <- sum + i
sum
}
seq(1 100 1)
)
will return the sum of values from 1 to 100 inclusive:
5050
This is a little clumsy though, and reduce
would be a better choice here, e.g.
reduce(λ(a:integer b:integer):integer{a+b}
seq(1 100 1)
0)
Finally, a general purpose while
construct is not strictly necessary since it could be implemented as a recursive function, e.g. as a procedure called while_
:
while_(condition:function body:function) {
if condition() then { body() while_(condition body) }
}
condition():boolean { i <= 10 }
body() {
println(format("%d^3 = %d" [i i^3]))
i <- i+1
}
i <- 1
while_(condition body)
But see also https://github.com/AAVSO/VStar/issues/364 for why this is not yet possible in VeLa.
Here is the full VeLa grammar (PDF).
Any text entry box in VStar that accepts a numeric value can instead receive a numeric VeLa expression.
Expressions such as 2457529.5-365.25*5
or today()-365.25*5
could be used as the minimum JD when loading a dataset from AID.
(obscode in ["CTIA" "SAH"] and band = "Johnson V" and uncertainty <= 0.01)
OR
(band = "Visual" and obscode = "BDJB")
This Boolean expression defines an observation filter in which:
- the observer code is CTIA or SAH and the band is Johnson V and the uncertainty is less than or equal to 0.01 or
- the band is Visual and the observer code is BDJB (the lead VStar developer)
Load eta Aquilae with a JD range of 2457529.5-365.25*5
to 2458259.5
and try this filter via VeLa Filter...
in the View
menu.
A VeLa observation filter has access to the properties of the currently loaded observations, such as time/JD, magnitude, observer code, band, and any properties peculiar to a particular observation source.
These expressions may also be used in the Observation List to restrict the set of observations, create selection filters, and exclude observations.
In addition, observation source dialogs allow a VeLa expression to be used to filter observations as they are loaded, rather than afterwards.
Applying DCDFT with Period Range to the eta Aql filtered observations above with a period range of 1 to 10 and a resolution of 0.01 will yield a period search top hit of 7.18.
This function defines a Fourier model obtained from a fundamental frequency of ~0.1393 (a period of 7.18) and two harmonics:
f(t:real) : real {
3.8893863
+0.1148971 * cos(2*PI*0.1392758*(t-2457506))-0.312022 * sin(2*PI*0.1392758*(t-2457506))
+0.0432131 * cos(2*PI*0.2785515*(t-2457506))-0.1196311 * sin(2*PI*0.2785515*(t-2457506))
+0.0370884 * cos(2*PI*0.4178273*(t-2457506))-0.0013007 * sin(2*PI*0.4178273*(t-2457506))
}
VeLa is used in conjunction with some plug-ins, for example:
- The VeLa observation transformation plugin requires a function to be defined to determine what transformation to apply to each observation.
- VeLa model functions are used in the VeLa model creator and VeLa observation source plug-ins.
- VeLa filters can be used to subset the observations loaded from an observation source.
See VStar Plug-in Details for more.
Here is an example of how to use the scatter
API function with VeLa:
# This is a comment.
# y <- x^3
x is seq(1.0 1000.0 1.0)
y is map(function(n:real):real{n^3} x)
scatter("Cubes" "x" "x^3" x y)
After entering this code and clicking Run
in the Tools
-> VeLa
... dialog, a new Cubes tab will be added with a plot of the cubes of x from 1 to 1000.
VeLa code can be entered into a dialog (Tools -> VeLa
) to test it before use in one of the contexts outlined above, e.g.
fact(n : integer) : integer {
when n = 0 -> 1
true -> n*fact(n-1)
}
map(fact [1 2 3 4 5 6 7 8 9 10])
π
pi
e
A range of in-built functions are available in VeLa and categorised below.
The help
function can be used to find out at least the parameter types and return type of a function. In some cases, a help comment is included, e.g.
help(ord chr)
gives:
ORD(character:STRING) : INTEGER
Returns an ordinal value given a single character string.
CHR(ordinalValue:INTEGER) : STRING
Returns a single character string given an ordinal value.
The next section summarises the available functions but help
can be used to find more information in some cases.
The function intrinsics
can be used to get a complete list of available intrinsic (vs user-defined) VeLa functions. The following code outputs one function per line:
for(println intrinsics())