This is the documentation for Beagle, a hypothetical programming language.
Beagle has scalar primitives.
int number = 0;
double money = 4.694;
bool is_hypothetical = true;
string hello = 'world';
void;
It also provides list
, record
and json
types for collections, structs, and dicts respectively.
Only record
is immutable.
record person(string name, int age);
person p = person("Person", 21);
list(int) numbers = [1,2,3,4];
numbers[1];
json my_response = { "name": p.name() };
my_response["age"] = p.age();
To work with these types, Beagle provides function. Beagle uses in
/out
parameters similar to Ada.
An example function to add 2 numbers is defined as
fn add {
in int a = 0;
in int b = 0;
out int sum = a + b;
}
A function can have any number of IN/OUT parameters. All IN parameters are read-only in the scope of the function and do not allow re-assignment or modifications. IN and OUT parameters can share the same name and can be differentiated in code by use of a namespace such as in::name
or out::name
.
Functions in Beagle are forever. A given function doesn't complete execution unless it's explicitly requested. What does that mean? Consider the above function add
, in JS, you'd execute it by
add(1,2) // 3
Trying to do the equivalent in Beagle does not give the same result,
add(1,2) // typeof 'add'
So when does the function execute? Accessing an OUT parameter completes an execution. Do note that, the function is still very much alive, you only get the value of the computation with the given IN/OUT parameters.
add(1,2)
.sum() // 3
.sum() // 3
.sum() // 3
Beagle supports partial application, so you can partially update the IN parameters at any point in time and trigger the computation.
add(1)
.sum() // 1, as b defaults to 0
.b(2)
.a(5)
.sum() // 7
.b(4)
.sum() // 9
IN parameters can be given through use of a Fluent style API.
You can kill the function through use of the !
operator.
add(1,2).sum()! // 3
add(1,2).sum()!.a(2) // TypeError: `int` is not a function
It is possible to define contract
(much like a type or interface). A contract for the add
function can be defined as,
contract add {
:in(int a, int b);
:out(int sum)
}
The names given for the IN/OUT parameters are part of the contract as well. Keep in mind, all IN/OUT parameters in Beagle are bound to a contract. Every function gets a default
contract that acts as the namespace for the IN/OUT parameters. The explicit notation for in string hello
would be default::in string hello
.
A function uses
contracts.
fn numbers uses add {
// This can be shortened to add::in if both the variable and contract use the same name.
add::in::a int a;
add::in int b;
add::out int sum = a + b;
}
Here int a
marked as add::in
is an IN parameter bound to the add
contract.
To invoke a particular contract against a function,
numbers.add(1,2).sum()
numbers.add.a(1).b(2).sum()
A function can uses any number of contracts and variables can be bound to multiple contracts.
contract add {
:in(int a, int b);
:out(int sum);
}
contract mul {
:in(int a, int b);
:out(int product);
}
fn numbers uses add, mul {
add::in mul::in int a;
add::in mul::in int b;
add::out int sum = a + b;
mul::out int product = a * b;
}
numbers.add(4,6).sum() // 10
.mul.product() // 24
.mul.a(3)
.add.sum() // 9
As a
,b
were bound as IN parameters for add
, mul
contracts, it was possible for either contract to use them freely.
Some more example code
fn greet {
in string name = "Greg!";
out string name = `Hello + ${in::name}`;
}
::print.stdout(greet.name('Casey!')!)!;
::print.stdout(greet('John')!)!;
fn sum {
in list(int) numbers = [0];
out int sum = 0;
for (int i = 0; i < ::length.of(numbers); i++) {
numbers += ::index.of(numbers).at(i);
}
}
::print.stdout(sum([1,2,3,4]))!
contract is_even {
:in(int number);
:out(bool is_even);
}
contract is_odd {
:in(int number);
:out(bool is_odd);
}
fn number_checker uses is_even, is_odd {
is_even::in is_odd::in int number = 0;
is_even::out bool is_even = number % 2 == 0;
is_odd::out bool is_odd = !is_even;
}
number_checker.is_even(3);