The Brain4it programming language (BPL) is inspired in functional languages like LISP or Scheme, that have been widely used over the years in the development of artificial intelligence applications.
BPL is a simplified version of these languages that depite of being much smaller, offers similiar possibilities.
Its main features are:
BPL only supports 3 basic data types:
true
or false
23473
or -8696
.87986256763423
or -39983266772324
.42.563
and -5.72e9
. The special values
NaN
(not a number), Infinity
and -Infinity
are also supported.
"Hello world"
.BPL also defines the literal null.
All these data types are immutable, which means that its value cannot be changed.
A reference is a name, symbol or path that references an object of any type.
There are two types of references:
if
, while
, sin
, +
, etc.
Hard references are not assignable, so they always point to the same built-in function.
alfa
, public/cars/23/model
, @dashboards
, get-value
, etc.
When a soft reference includes slashes (/) it is a path reference. Path references indicate a path through a list sctructure to access the data. The components of a path reference can be strings to access by name or integers to access by index within the list, as discuss below.
The name of a non path reference may contain any character except space, slash (/), double quote (") and parenthesis and the first character can not be a digit.
Soft references can not match the name operator =>
, a built-in function or the literals
null
, true
and false
, NaN
, Infinity
and -Infinity
.
All references are immutable because its name can not be changed.
The list is the only data structure supported in the Brain4it programming language.
Lists allow you to represent both data and code. They are expressed by parenthesis and may contain from 0 to N elements of any type, separated by spaces. Examples:
()
an empty list.(1 2 3 4)
a list that contains 4 numbers.("red" "green" "blue")
a list containing 3 strings.(12413 data-on ("on" "of") (3 8 5) 7.8)
a list that contains elements of different types and other lists.(+ (* 8 a) (* 2.4 b))
a list containing the mathematical expression 8 * a + 2.4 * b.(while (< a 10) (push elems a) (++ a))
a list that encodes a fragment of a program.Elements in a list can be labeled with a name. That name is a string that must be unique within the list.
The arrow (or name) operator =>
is used to set a name for an element. Examples:
("temp" => 25.32)
"temp" is the name for the element 25.32("us-dollar" => 105.42 "yen" => 0.34)
a list that contains 2 numbers labeled with names "us-dollar" and "yen".("red" => "FF0000" "green" => "00FF00" "blue" => "0000FF")
a list that contains 3 strings labeled with names "red", "green" and "blue".(number "100100100" "radix" => 2)
a list with 3 elements where the last one has the name "radix".Elements in a list can be accessed by position or by name.
For example, if we assign the variable persons
this list:
(("name" => "John" "age" => 34 "0" => 45) ("name" => "Mary" "age" => 31))
persons/0
will point to ("name" => "John" "age" => 34)
persons/0/age
will point to 34
andpersons/1/name
will point to "Mary"
persons/0/"0"
will point to 45
Later we will see in more detail how to access the elements of a list.
A list is mutable because you can add and remove elements to it.
The Brain4it programming language has the following evaluation rules:
Examples:
null
evaluates to null
true
evaluates to true
4.38
evaluates to 4.38
"Hello world"
evaluates to "Hello world"
()
evaluates to ()
(1 2 3)
evaluates to (1 2 3)
(+ 3 4)
evaluates to 7
. + is the built-in sum function, and numbers 3 and 4 are the arguments passed to that function.(if (> 3 2) "GREATER" "LOWER")
evaluates to "GREATER"
.(sort (4 1 7 2))
evaluates to (1 2 4 7)
.(data 3 4)
evaluates to (data 3 4)
because data
is not a function.(fact 5)
evaluates to 120
because fact
is a user function that implements the factorial.The Brain4it programming language has 2 types of functions:
BPL has built-in functions to perform basic operations like assigment of variables, conditionals and loops. Also offers a rich set of functions to work with lists, strings, numbers and dates. Next we will see some of the most common built-in functions:
The assigment of variables is done through the set
function.
(set result (+ 4 5))
In this example, the variable (or reference) result
takes the value 9.
The set
function, like many other built-in functions, do not evaluate
the reference that is going to assign a value (result
in the example).
Only when the first argument is not yet a reference the set
function will
evaluate it to get the final reference, like in this example:
(set (get result) 8)
where the value 8
will be assigned
to the reference pointed by result
.
A conditional instruction can be done with the if
function. Its syntax is:
(if condition action_when_true action_when_false)
Example:
(if (> level 7.5) "OK" "KO")
If the value of level
is greater than 7.5
the funtion will return
"OK"
, otherwise will return "KO"
.
A sequence of actions can be grouped with the do
function:
(if (> level 7.5)
(do
(send_email "OK")
(switch_alarm 0)
)
(do
(open_door)
(send_email "KO")
(switch_alarm 1)
)
)
When you have to code an if-then-else cascade its more convenient to use the cond
function:
(cond
(when condition_1
action
...
action
)
...
(when condition_n
action
...
action
)
)
The cond
function only executes the actions of
the first when
clause which condition evaluates to true
.
Example:
(cond
(when (= state 1)
(set speed 4)
)
(when (= state 2)
(set speed -4)
)
(when true
(set speed 0)
)
)
In that example, if the initial value of state
is 1
the variable speed
will take the value 4
.
For doing loops, BPL has the while
function, that works
like the while
statement of many other programming languages:
(while condition
action
...
action
)
while the condition evaluates to true
, the actions inside the while
will be executed.
Example:
(while (>= a 0)
(-- a 2)
)
In that example, variable a
will be decremented by 2
until its value be less than 0
.
An alternative to the while
function is the for
function,
that has a syntax pretty similiar to the for
statement of C like languages:
(for
initialization_code
condition
iteration_code
action
...
action
)
that is equivalent to:
(do
initialization_code
(while condition
action
...
action
iteration_code
)
)
Example:
(for (set i 0) (< i 10) (++ i)
(prepare_data i)
(send_data i)
)
That code will execute prepare_data
and send_data
10 times.
As you probably have guessed, the arguments of a built-in function are not evaluated "a priori". The function evaluate them only when it considers that it is necessary. Other programming languages like C, Java or Javascript work different because they evaluate all the arguments before calling a function.
The more common built-in functions to manage lists are list
,
get
, put
and remove
.
The list
function creates a new list and adds to it the evaluation of its arguments:
Example:
(list (+ 3 4) "a" => (- 5))
will return (7 "a" => -5)
The get
function allows you to obtain an element of a list
accessing by index or by name.
Its general form is:
(get lst spec)
where lst
is the list, and spec
is the index or name.
Examples:
(get (1 2 "a" => 3) 1)
In that example the function will return the element at index 1
,
which is the value 2
(the index of the first element is 0).
(get (1 2 "a" => 3) "a")
here the get
function will return the element whose name is "a"
,
which is the value 3
.
To put an element into a list, BPL has the put
function. Its syntax is:
(put lst spec value)
where lst
is the list, spec
is the index or name
and value
the element to put.
Examples:
(do
(set my_list (1 2 "a" => 3))
(put my_list 0 "Mary")
(put my_list "a" "Rick")
(put my_list 5 99)
)
After executing that code, variable my_list
will be:
("Mary" 2 "a" => "Rick" null null 99)
When the specified index is outside the size of the list,
that list is automatically enlarged to put the element, filling with null
the new positions in the list.
Removing elements from a list is done with the remove
function, that has this syntax:
(remove lst spec)
where lst
is the list, and spec
is the index or name of the element to remove.
Built-in functions are organized in libraries. The Core library contains the basic functions that all programs need and is always present in the Brain4it servers.
Some other libraries may also be available to access external services or hardware resources. See the Library catalog for more information.
An anonymous user function is defined this way:
(function (parameter ... parameter)
action
...
action
)
where the parameter
elements represent the
parameters that takes the function,
and the action
elements the actions to execute.
The value returned by the function is the result of evaluating the last action.
Example:
(function (x y) (* x (sin y)))
That function takes two parameters x and y and will return the result of evaluating x * sin(y).
We can invoke an anonymous function using the call
function:
(call (function (x y) (* x (sin y))) 45.2 3.6)
To give that function a name, assign the function to a variable:
(set sin_product (function (x y) (* x (sin y))))
and then invoke the function with a calling list like this:
(sin_product 45.2 3.6)
The parameters of a user defined function can be defined in three ways:
x
) : the value of the parameter (x
) is taken from the
calling list accessing it by the position of this parameter."n" => x
) : the value of the parameter (x
)
is taken from the calling list accessing it by the name of the reference ("n"
)."x"
) : the value of the parameter (x
) is taken from the
calling list accessing it by this string ("x"
). That is equivalent to "x" => x
.When a parameter is not specified in the calling list, its value
will be null
.
For example, if we want to define a function that takes a mandatory
parameter value
and 2 optional parameters offset
and factor
,
we can define it like this:
(set fn
(function (value "offset" "factor" => f)
(* f (+ value offset))
)
)
If we invoke that function with this calling list:
(fn 7)
offset
and f
will be null
and
the function will return the value 7
, but in this case:
(fn 7 "offset" => 5)
only f
will be null
and the result will be
12
. In this other call:
(fn 7 "factor" => 2)
offset
will be null
and the returned value
will be 14
and here:
(fn 7 "offset" => 5 "factor" => 2)
we will obtain the value 24
.
User defined functions unlike built-in functions receive
its arguments already evaluated. If you need to pass an expression
to a user defined function without evaluating it, use the quote
function.
Example: (process_data (quote (+ 1 2)))
. The process_data
function
may evaluate that argument with the eval
function.
BPL supports recursion, that is functions that call themselves.
For example, the factorial function can be defined in this way:
(set fact
(function (x)
(if (< x 1) 1 (* x (fact (- x 1))))
)
)
BPL supports exceptions as the Java programming language.
Exceptions are thrown in these cases:
kill
function.throw
functionExceptions can be handled with the try
function, that works
like the Java try-catch-finally statement. Its general syntax is:
(try
main_action
(ex_var catch_action ... catch_action)
finally_action
)
Let's see an example:
(set res (get_resource))
(try
(do
(process_resource res)
(notify_result res)
)
(ex
"ConnectException" => (abort_process)
"SQLException" => (abort_process)
"IOException" => (process_resource res)
"*" => (notify_error ex)
)
(release_resource res)
)
When an exception occurs the ex_var variable (ex
in the example) will contain a list like this:
(
"ConnectException"
"message" =>
"Can't connect to http://brain4it.org"
"code" => (http "GET" url)
"stack" => (do_work process_resource)
)
where the first element is the exception type (usually the simple name of the Java Exception class) and the next elements (not always present) are the detail of the exception:
The executed catch_action is that whose name matches the exception type ((abort_process)
in the example).
If there is no catch_action that matches the exception type, then the catch_action
whose name is "*"
will be executed.
If the main_action throws an exception that is not handled by any catch_action that exception will be propagated to the superior function.
The finally_action will always be executed whatever an exception is thrown or not.
The value returned by the try
function is the result of
evaluating the main_action or in case that main_action throws an exception,
the result of evaluating the catch_action that handles that exception.
The try
function differs from the Java try-catch-finally statement in these aspects:
kill
function always force the interruption of the executor thread.The throw
function allow us to throw an exception. Example:
(throw "UserError" "The value entered must be positive")
where the first argument is the exception type and the second argument (optional) is the message of the exception.
The throw
function can also accept an exception list as the first argument:
(throw ("UserError" "message" => "The value entered must be positive"))
The Brain4it programming language supports running code in multiple execution threads.
Every time a command is launched, the server creates an executor thread that will run that command until it ends.
We can list all the executors currently running within a module with the executors
function:
(executors)
the result could be a list like this:
("45" => (while true (eval rules) (sleep 1000)) "47" => (executors))
This list contains for each executor the code that it is running labeled with the executor_id, the executor identifier.
If we want to interrupt the executor identified by "45"
, simple type:
(kill 45)
This command invokes the Thread.interrupt() Java method on the executor thread to force its finalization.
Sometimes we want to run a code without waiting for the result. That
can be achived with the spawn
function:
(spawn (do (sleep 5000) (beep))
spawn
creates the new executor thread and immedialety returns its executor_id.
To synchronize multiple executor threads, BPL offers functions similiar to those
that exists in the Java programming language: sync
, wait
and notify
.
See the core library documentation for details.
All executor threads running inside a Brain4it server see two variable scopes:
Every time we call a function, a new local scope is created. The arguments of the function are put into that scope. When the function returns, the scope is destroyed.
If a variable is set with a statement like this:
(set alfa 9)
that variable will be created in the global scope (also inside a function) and thus will be visible by other executors in the same module.
If we want to create a variable in local scope we must declare the variable this way:
(local alfa)
That variable will last as much as the local scope where it was created.
Variable scopes in BPL are implemented as lists.
We can obtain the global scope list with the global-scope
function:
(global-scope)
that may return a list like this:
("alfa" => 9 "square" => (function (x) (* x x)))
The local scope is obtained with the local-scope
function.
Both scopes, global and local, can be manipulated as normal lists.
To remove a variable from the scope where it is defined,
you can use the delete
function:
(delete alfa)
To check if a variable is defined in the current scopes (local or global),
use the exists
function:
(exists alfa)