By Sergio Pedraza @sergiouph
Bm is a general purpose programming language designed to be simple to write, read and maintain. It can be considered a multi-paradigm language since it is focused to provide robust solutions by enforcing principles not limited to a specific paradigm.
The design of the Bm Language is driven by a set of principles explained in this section. The principles were not just picked from the best-practices, they were synthesized organically from the evolution of the design of the language.
Every symbol in the code is defined in some place which can be found using simple rules.
A member can't be used if it is not explicitly imported or defined, even the primitive data types are not “automatically imported” to the modules, this forces the developer to import and track what specifically is used in a suite. Despite this may sound tedious and against the readability principle, the developer may leverage the access system and create a source file exclusively for importing the most used members across the suite, this may significantly improve the developer experience since some members can be used in an "auto-imported" fashion and be trackable at the same time.
import Int from bm let a Int = 0 // OK let b String = "" // ERROR! `String` is not defined
String
is not defined since it is not imported.
Since shadowing is disallowed, the references can't be ambiguous.
let enabled = true class Test { let enabled = false // ERROR: `enabled` is already defined }
enabled
was defined in an upper level, the
class Test
cannot define a member with the same name.
Fields must be initialized, there are no default values.
class User { var id Int // ERROR! `id` needs to be initialized }
id
causes a compilation error because it requires an initial value.The written code must be easy to read and the developer must write the minimum amount of code.
The type inference allows to omit the type in most cases. When the type is not specified, it is determined by the initial value. The inconvenient with omitting the type is that sometimes the inferred type can be too wide.
let count = 0; // type: Int let name = "Mat"; // type: String let rect = Rectangle.new(0, 0, 10, 10); // type: Rectangle let a = null; // type: Any
Parenthesis after an if
, for
, while
, etc., and separators between instructions
are not required.
function main(args Array<String>) { if args.length == 0 { println("Missing arguments") } }
if
block doesn't require parenthesis for the condition.
Extensions allow to have large class definitions and even so have small files.
// file: user.bm class User { let id Int constructor new(@id Int) { id = @id } } // file: user.delete.bm function User.delete() { /* ... */ } // file: main.bm function main() { User.new(10).delete(); }
delete
is a member of the User
class
despite they are defined in different files.The language helps to reduce the amount of mutable members by forcing the developer to indicate it explicitly.
Parameters in functions are constant by default, variable parameters are allowed only by declaring
them with the var
modifier. When the modifier is omitted it is assumed to be let
.
function test(a Int, var b Int) { a += 1 // ERROR: `a` cannot be reassigned b += 1 // OK: it was declared with `var` }
a
parameter can't be reassigned unlike b
that
was explicitly declared to hold a variable value.
Interfaces may help to control if a field is mutable or not. When a type is treated as some
interface, var
fields in the original implementation can fit in let
fields (disallowing
the assignment), in contrast, let
fields wouldn't fit in var
fields causing a compilation error.
class Human { var name = "Mathison" // It can be reassigned } interface Dog { let name String // Cannot be reassigned } function test() { let h = Human() let d Dog = h // Now it is a Dog! Not a Human d.name = "Amy" // ERROR: `name` cannot be reassigned }
Dog
disallowed the reassignment of the field name
despite the original implementation allows to be reassigned.
When a constant field is defined in a class, the initialization can be delegated to the constructor. This allows to have constant fields with computed values instead of variable fields initialized with some value and then reassigned in the constructor with the computed value.
class Cat { let name String // OK: initialization is delegated to the constructor constructor new(@name) { name = @name // OK: assignment must happen only in this block } }
name
field was initialized in the constructor.
Declaring constants is as simple as declaring variables since they require the same amount of
characters: let
for constants and var
for variables. This matters — some languages require
extra effort to declare constants.
Language | Variable example | Constant example |
---|---|---|
Bm, Swift | var c = 0 | let c = 0
|
Rust | let mut c = 0; | let c = 0;
|
Go | var c = 0 | const c = 0
|
JavaScript, TypeScript | let c = 0 | const c = 0
|
C, Objective-C | int c = 0; | #define c 0
|
C++, C# | int c = 0; | const int c = 0;
|
Java | int c = 0; | final int c = 0; |
Interfaces help to describe how a value should look like without specifying a real type and templates help to inject types as parameters that can be used to write generic code. Both features may help to reduce the dependencies used in a suite.
// file: entities.bm module entities { share class User { let key String constructor new(@key String) { key = @key } } } // file: storage.bm module storage { share interface Item<ID> { let key ID } share class Container<ID> { function save(item Item<ID>) { // It only requires something with a key so it can be saved. } } } // file: main.bm import Container from storage import User from entities function main() { let container = Container<String>() let user = User("3e3b3142") container.save(user) }
entities
and storage
don't share anything, however, they can be used together.
Extensions make possible to add features to existing classes and interfaces without changing the original definition. The extension member is recognized as a normal member of the type as long as it can be accessed from the module where it is going to be used.
// file: entities.bm module entities { share class Artist { var firstName String var lastName String } } // file: main.bm import Artist from entities getter Artist.fullName String { return "${this.firstName} ${this.lastName}" } function main() { let a = Artist(firstName: "John", lastName: "Coltrane") assert a.fullName == "John Coltrane" }
fullName
field is recognized despite it was defined in a different module.
Even though multiple inheritance is disallowed, a class or interface can be composed
explicitly with any amount of other interfaces. This can be done when the type is
being defined or after the definition using the compose
feature.
import Array from bm interface Item { operation gt(other Item) Bool } interface Sortable<T Item> { let length Int operation get(index Int) T operation set(index Int, value T) function bubble_sort() { for i = 0 while i < length step i++ { for j = 1 while j < (length-i) step j++ { if this[j-1] > this[j] { let aux = this[j-1] this[j-1] = this[j] this[j] = aux } } } } } compose Array<T> implements Sortable<T> function main() { let items = [3, 1, 2] // Produces an Array<Int> items.bubble_sort() assert items == [1, 2, 3] }
bubble_sort
is recognized since the class Array
was
composed with the Sortable
interface.Bm can be considered as a part of the C-family languages, the code written in Bm is free-form, there is no pattern to follow regarding the organization of the source files.
Bm projects, also named Suites, can be organized using two concepts: Members and Containers. Some containers are also members and some members are flat.
The definition of the members and containers of a suite depends on the directory organization and the content of the files. All files and directories are used for defining the suite.
The containers are used for collecting members, all containers have a name, but not all containers can be referenced in the code.
The root container for all members and containers is the suite, which can be considered as a module that cannot be referenced. A directory containing at least the suite file is considered a valid suite definition.
Besides the suites, there are other containers defined by using the file system:
There are other containers that can only be defined by writing Bm code:
module
keyword and can contain any kind of member.
Module definitions are not unique, the members are merged when any other kind of
module definitions are found.
Members are owned by a container and have a name. The name of a member in a container must be unique, however, two or more members can be grouped using the same name when they are overloaded or when they are a getter and setter pair.
Classes, interfaces, enums and modules are members that are containers as well. The modules are the only members that can be nested indefinitely. Classes, interfaces and enums are owned exclusively by modules.
Members listed below can be overloaded, in other words, multiple definitions can share the same name as long as the overloading rules are followed:
Fields are the most simple members since they can't receive parameters or have a body. They can be defined in modules and classes, behaving just like the functions.
Getters and Setters can share the same name since, together, they can be treated like a field. They can be defined in modules and classes as well.
Suite is a Bm concept for representing projects, solutions, programs or packages. This concept is important since Bm is not designed for scripting, said in other words, the design doesn't promote having stand-alone files:
Suites can be compiled to a distributable file format containing the necessary information to be imported in other suites or executed in any platform. Only suites containing an Entry Point can be executed, however, all suites can be imported in other suites.
A suite file is a JSON file named bm.json
, it describes the suite represented
by the folder where the file is. This folder is considered the Suite Module.
Name | Type | Description | Default value |
---|---|---|---|
id
| String | It is an UUID identifying the suite, it is analogous to a fingerprint. This value should never be changed since it is used to validate the correctness of a suite reference. | Random UUID. |
name
| String | The name of the Suite Module. This name is used by other suites for importing the members from this suite. | The name of the suite folder. |
version
| String | Is a value composed by three numbers in the following format: major.minor.patch. The numbers between a versions should be incremented by following these criteria:
| 0.0.0
|
Despite previous fields are required, they are auto-filled with the default values when they are missing.
A suite can be executed only if it contains an entry point which normally is a function
in the suite module named main
, however, it can be changed in the suite file.
// main.bm function main() { // program logic }
This system defines the rules for referencing members in the code based on the relationship between containers and members. Referencing a member doesn't guarantees the access to it, the access to the members is managed by the Access System.
Since all members have a name, containers can be viewed as a list of names and each name could contain more names since some members are containers too. This structure of names is delimited by following rules:
Any name can be given to a member, but the way to represent the names in the Bm Language obbey two formats:
`
).
The characters that are considered letters are
from a
to z
(lowercase), from A
to Z
(uppercase), _
(underscore), $
(dollar sign) and @
(at symbol).
The delimited string follows the Character Escaping rules.
TODO Add information for operators.
Since the name of a member is unique, members can be referenced unambiguously using the member path
which can be generated by joining the path of the container and the name of the member with a point (.
),
containers without name are ignored.
// Member Path: module math { // "math" let PI = 3.141592; // "math.PI" function max(a int, b int) int { // "math.max" return a > b ? a : b; } module geo { // "math.geo" function tan(value double) double { // "math.geo.tan" // fancy maths... } } }
Naming a member with the same name than a member that can be referenced is not allowed because it generates ambiguity.
let x = 0; class Point { var x = 0; // ERROR: The member "x" is already defined in the parent module. var y = 0; function moveTo(@x, y) { // ERROR: The member "y" is already defined in the class "Point". let Point = null; // ERROR: The member "Point" is already defined in the parent module. } }
This system refers to how the members are accessed through the modules. This is controlled by using a set of keywords called access modifiers which define the rules to access to some member from a certain module.
Who can access to the member? | protect | (default) | share | export
|
---|---|---|---|---|
Same declaring container | ✅ | ✅ | ✅ | ✅ |
Same module in the same suite | ❌ | ✅ | ✅ | ✅ |
Any module in the same suite | ❌ | ❌ | ✅ | ✅ |
Any module in any suite | ❌ | ❌ | ❌ | ✅ |
The protect
modifier is the most secure modifier, the access to members declared with this modifier
is limited only to the container where the member was declared,
they can't be imported or accessed by other source files.
class Foo { protect let BAR = 1; } protect let BAZ = Foo().BAR + 1; // ERROR: `BAR` is not accessible
protect let QUX = BAZ + 1; // ERROR: `BAZ` is not accessible module abc { let XYZ = QUX + 1; // OK! `QUX` is accessible }
The default modifier is the most handy modifier, to declare a member with this kind of access the member shouldn't have an access modifier at all.
This modifier makes that the member can be accessed only by the members defined in the same module or submodules of the same suite, other modules can't import or referencing the member. It keeps being secure, in the sense that it encapsulates the member in the declaring module.
let FOO = 1;
module bar { let BAZ = FOO + 1; // OK! `FOO` is accessible } let QUX = bar.BAZ + 1; // ERROR: `BAZ` is not accessible
The export
modifier should only be used when the member needs to be exported outside the suite.
Members declared in the same module or submodules have access to the declared member without importing it
and the member can be imported in any module of any suite.
export function foo() { /* ... */ } share function bar() { /* ... */ }
import foobar.foo; // OK! `foo` is exported import foobar.bar; // ERROR! `bar` is not exported
A module is a container for Fields, Functions, Classes, Properties, Enums, Interfaces, Extensions and more Modules. The access to the members is determined by its module access and they are always available during the runtime.
The global module cannot be referenced inside of the suite, however, when using another suite as dependency, the global module of the other suite can be referenced by the name of the suite.
// the member is declared in the global module of the "foobar" suite export function doSomething() { /* ... */ }
// the global module of the library "foobar" is now "foobar" import foobar.doSomething; export function main() { doSomething(); }
The explicit modules are created by using the keyword module
.
module math { // ... }
Modules are defined at file system level by using directories. The root directory is considered the global module and all other modules are relative to it. The source files contained in a module define the corresponding members.
/some/path/ |- awesome-app/ : Global module (root directory) | |- math/ : Module 'math' | | |- physics/ : Module 'math.physics' | | | |- mechanics.bm | | | |- fluids.bm | | \- geometry/ : Module 'math.geometry' | | |- topology.bm | | |- trigonometry.bm | |- data/ : Module 'data' | | \- local-storage.bm | \- main.bm
Shared and exported members can be imported into another module.
The imported members become also members of the module which is importing them.
To avoid adding complexity, the imported members cannot be renamed.
The module access rules applies as well as any other definition,
this means that the import
statements can be declared with a module access modifier:
Protected imports: are useful when it is required to use a member from another module or suite that it is not going to be widely used, so in order to use it, it must be imported in every container where is required.
protect import math.max; // this member will only be used here export function validatePort(port Int) Bool { return max(port, 0); }
Default imports: are useful to create the environment of the module by importing members that will be used in the module and submodules.
// following members will be widely used across the module import assert, log, inspect from debug;
Shared imports: are useful for composing a module or avoiding name collisions.
module impl { // need to be shared to expose them outside this module share import somelibrary.log; share import otherlibrary.timestamp; } function log(message String) { impl.log("${impl.timestamp()} - ${message}"); }
Exported imports: are useful to re-distribute a member.
// need to be exported so `getTypeName` can be exported as well export import constants.Type; export function getTypeName(type Type) String { /* ... */ }
When importing a member it is just linked so no clones of the member are generated. At the end the real module owner of the member is where the member was defined, not where it was imported. This implies that the access rules depend on the real module, not the importing one.
module AAA { share class Foo { let BAR = 1; } } module BBB { share import AAA.Foo; } module CCC { function test() { log(AAA.Foo.BAR); // ERROR let Foo.BAR is not shared log(BBB.Foo.BAR); // ERROR let Foo.BAR is not shared } } // Notice the module is the same than the owner of Foo module AAA { function test() { log(Foo.BAR); // exactly the same than the line below log(AAA.Foo.BAR); // OK! it has access log(BBB.Foo.BAR); // OK! it has access } }
Multiple members can be imported using the same sentence, if the module is indicated all imported members must be of the same module.
module A { share module B { share let X = 1; share let Y = 2; share let Z = 3; } share module C { share let I = 1; share let J = 2; share let K = 3; } } module example1 { import A.B.X, A.B.Y, A.B.Z, A.C.I, A.C.J, A.C.K; } module example2 { import B.X, B.Y, B.Z C.I, C.J, C.K from A; } module example3 { import X, Y, Z from A.B; import I, J, L from A.C; } module example4 { import A.B, A.C; }
When a module is imported, all members of the module becomes members of the importing module.
Extensions allow to add members to existing types without modifying the existing definition or creating a new type.
The way to define extensions is by prefixing the member name with the type reference followed by two colons (::
),
only members with logic can be used as extensions:
The body of the extension member, just like Class Members, has access to this
representing the instance
and all other instance members will be available.
class User { var name String = null; } function User::delete() { Console.log("Deleting user ${name}"); // OK! `name` is available }
When an extension is defined in the same module and suite than the extended type, the extension member will be treated as a member of the type, so when the type is imported it will have the extension members available.
module lib { share class Collection { /* ... */ } share function Collection::clean() { /* ... */ } } module app { import Collection from lib; function test() { Collection().clean(); // OK! `clean` is available } }
If the extension member is declared in a different module than the extended type, it needs to be imported explicitly so the type can have the extension member available.
module lib { share class Collection { /* ... */ } module actions { share function Collection::clean() { /* ... */ } } } module app { import Collection from lib; import User::clean from actions.lib; // this line is required for the extension function test() { Collection().clean(); // OK! `clean` is available clean(); // ERROR: `clean` is not defined } }
Generic programming is implemented by adding a template signature to
Classes, Interfaces, Lambdas, Functions and Operators.
The template signature can define one or more Template Types associated with another type,
when the associated type is not specified it is assumed to be Any
.
Template Types are considered valid types in the inner implementation.
Template Members can only be used if the Template Arguments are clear, it can happen in two ways:
interface Comparable<T> { operator `==` (value! T) Bool; } interface Entity<ID> { var id Comparable<ID>; } class User { var id Int; } class Container<ID> { function store(entity Entity<ID>) { /* ... */ } } lambda Storer<T>(item T); function createStorer<ID>() Storer<ID> { let container = Container<ID>(); return container.store; } function main() { let store Storer<Int> // Explicit Template Arguments = createStorer(); // Inferred Template Arguments let user = User(); store(user); }
Members that accepts parameters can be overloaded, this means that more than one definition can be provided with the same name as long as the parameters at the same position are not compatibles with each other. There is no restriction on the number of parameters or if they are optional.
function print(value Any) { /* ... */ } function print(value String) { /* ... */ } // ERROR! `String` is compatible with `Any`.
function max(a Int, b, Int) Int { /* ... */ } function max(a Single, b Single) Single { /* ... */ } // OK! `Int` and `Single` are not compatibles
Overriding inherited functions in classes is disallowed, however the way of adding behaviour is by using before
and after
.
Since in Bm is not possible to override an inherited function, an alternative might be to add aspects to the class so it can be possible to execute some block of code before or after the function is called.
Aspects should be directly added only to inherited functions to avoid obscuring the logic of normal functions.
The block of code to be executed before or after the function should not have return type, the parameters for before blocks, should be exactly the same than the target function and the parameters for after blocks, should be the same than the target function plus the return type only when it applies. In this way, aspects will be able to mutate the input and output of a function, but won't be able to change the references.
Only one aspect per before and after should be allowed in one class. The aspects added to functions which the superclasses already added aspects will be chained.
class Shape { var x = 0; var y = 0; function moveTo(@x Int, @y Int) { x = @x; y = @y; } } class Element { function redraw() { /* ... */ } } class Oval inherits Element, Shape { after moveTo(@x Int, @y Int) { redraw(); } var width = 0; var height = 0; function draw(g Graphics) { g.drawOval(x, y, width, height); } }
All fields must be initialized explicitly by the developer, there is no concept of implicit default value like 0
, false
or null
.
A field can be only initialized by using one of the three ways described below.
This is the simpler way, the value is specified immediately after the declaration.
var count = 0; let user = User.new("Mat");
The initialization of the fields in a class can be delegated to the constructors.
The initial value for the field can be omited in the field's declaration, but the class must declare one or more constructors and each constructor must initialize the field.
class Entity { let id String; // initialization delegated constructor new(@id String) { id = @id; // initial value } }
See Constructors for more details.
Despite a function parameter can be considered as a field, the initialization is optional since the initial value is taken from the function invocation.
However, initial values in function parameters are allowed but it indicates an Optional Parameter.
function test(a String, b String = "bar") { // do something } function main() { // the initial values will be: // a = "foo" and b = "bar" test("foo"); }
See Function Parameters for more details.
Bm is a language statically typed, all values are resolved to a type at compiling time. The usage of interfaces and templates makes possible to write code for abstract types, however, at compiling time the type abstractions are resolved to well-defined types.
Following list shows the types that can be declared:
In Bm all values are passed by reference, this means that if a value is passed to a function, the function is able to modify the received value without creating a copy.
There is no way to indicate that the value should be passed by value instead of reference (creating a copy), however, the compiler, depending on the platform, can decide how to pass the value based on the mutability of the value.
TODO: Values can be null only if they are optional.
In Bm all references can be null by default no matter the type of the field,
the only way to avoid null values in a field is declaring it with the non-null modifier (!
) after the name.
When a field has a null value, it means that the field doesn't have a valid value.
If a field is declared as non-null, it is guaranteed to never hold a null value.
Null value assignments to non-null fields will throw an error at runtime,
compilation errors only happen if the assignment can be detected by the compiler.
All values' types can be inferred at compiling time, but in some cases the type has to be declared explicitly
to avoid generalities or specificity. The most generic value that can be assumed is Any
and
the most specific will depend on the initial value.
// if the type is not specified it would be Any var name String = null; // if the type is not specified it would be bool var value Any = true;
Type casting makes possible to treat a value as if it was of other type without creating another value. Syntax:
(TargetType)sourceValue
When the type of the source value and the target type are not compatible an error is generated.
TODO: Complete adding examples and indicating if it is generated at compiling time, runtime or both.
TODO Explain that can be implemented by the compiler, list expected natives, explain how to pass values from bm.json to runtime.
Bm is not oriented to a specific platform or hardware, however, in order to operate it defines a set of native classes which are assumed to exist and can be used to create more complex types.
Type | Size | Description | Range | |
---|---|---|---|---|
Byte | 8 bits | Smallest signed integer. | −128 to +127 | |
UByte | 8 bits | Smallest unsigned integer. | 0 to 255 | |
Short | 16 bits | Medium-sized signed integer. | −32,768 to +32,767 | |
UShort | 16 bits | Medium-sized unsigned integer. | 0 to 65,535 | |
Int | 32 bits | Default signed integer. | −2,147,483,648 to +2,147,483,647 | |
UInt | 32 bits | Default unsigned integer. | 0 to 4,294,967,295 | |
Long | 64 bits | Biggest signed integer. | −9,223,372,036,854,775,808 to +9,223,372,036,854,775,807 | |
ULong | 64 bits | Biggest unsigned integer. | 0 to 18,446,744,073,709,551,615 | |
Single | 32 bits | Single-precision floating-point number | ||
Double | 64 bits | Double-precision floating-point number | ||
Bool | 8 bits | true or false | ||
Char | 16 bits | Any unicode character. | ||
String | Depends on the instance. | Stores a sequence of Chars. | ||
Array<T> | Depends on the instance. | Stores a sequence of values of the same type. |
As well as specific classes are expected to exist, some interfaces are necessary to provide complete and decoupled support of some features of the language.
Type | Description |
---|---|
Any | Represents any value. |
Tuple<…> | Stores a sequence of values of specific types per item. |
Error | Describes an error at runtime. |
Numbers:
function main() { let way1 = (Byte)127; let way2 Byte = 127; let way3 = (n Byte) => {}; way3(127); }
Classes are custom types that can be instantiated into values using constructors. All other members declared in the class than the constructors can only be accessed through an instance of the class. Only some Built-in Classes can be created using literals, without invoking a constructor.
class User { let id Int; var name String = null; var email String = null; constructor new(@id) { id = @id; } function delete() { /* ... */ } getter displayName String { return "${name} <${email}>"; } operator == (other! User) Bool { return this.id == other.id; } }
class User { let id Int; var name String = null; var email String = null; constructor new(@id) { id = @id; } } function User_delete(user User) { /* ... */ } function User_get_displayName(user User) { return "${user.name} <${user.email}>"; } function User_equals(User left, User right) { if left == null { return right == null; } else if right == null { return false; } else { return left.id == right.id; } }
TODO remove multi-inheritance and allow interfaces to have defined functions (but not initialized fields)
Classes can inherit from one or more classes only following these rules:
NOTE: See Type System for more details.
// represents an identifiable thing class Entity { export let id String; share constructor new(@id String) { id = @id; } } // represents a record with control fields class Record { export let createdAt Date; share var $updatedAt Date = null; share constructor new() { createdAt = Date.now(); } export getter updatedAt Date { return $updatedAt; } } // represents the user entity record class User inherits Entity, Record { protect var $name String = null; // must be chained to the superclasses' constructors export constructor new(@id String) : Entity.new(@id), Record.new() {} export getter name { return $name; } // non-protected superclasses' members can be accessed export setter name(@name String) { $name = @name; $updatedAt = Date.now(); } } function test() { let user = User.new("0001"); console.log(user.id); // prints 0001 console.log(user.name); // prints null console.log(user.updatedAt); // prints null console.log(user.createdAt); // prints a Date user.name = "Mat"; console.log(user.name); // prints Mat console.log(user.updatedAt); // prints a Date }
TODO: Keyword extends
. Only one superclass allowed. Inherits everything even the constructors.
Despite it is not necessary, a class can explicitly implement one or more interfaces. The compiler will verify that the class comply with the definition of the interfaces. The interfaces specified will obbey the Interface Inheritance rules. If a function specified by an interface is not implemented, the class will be trated as Incomplete Class.
interface Drawable { function draw(g Graphics); } interface Shape { getter origin Point; getter size Size; } class Rectangle implements Drawable, Shape { export var origin Point = null; export var size Size = null; export function draw(g Graphics) { g.drawRect(origin.x, origin.y, size.width, size.height); } export constructor new() {} } class Oval { export var origin Point = null; export var size Size = null; export function draw(g Graphics) { g.drawOval(origin.x, origin.y, size.width, size.height); } export constructor new() {} } function test() { let g = Graphics.new(); let shapes = ArrayList<Shape>.new(); shapes.add(Rectangle.new()); // next line is correct despite it was not explicit in the class definition shapes.add(Oval.new()); for shapes : shape { shape.draw(g); } }
Since classes can inherit from other classes, the complete definition can be delegated to subclasses. Rules:
Interfaces are descriptors of types. Any value can be treated as an interface without explicit casting as long as the type of the value matches with the definition of the interface.
interface Name inherits SuperInterface1, SuperInterface2 { function doSomething(param1 ParamType) ReturnType; getter someField Type; setter someField(value Type); }
Interfaces can only contain Functions, Properties and Operators.
Interfaces can inherit from other interfaces, however if two or more superinterfaces have a member with the same name they must exactly match, otherwise it will cause an inheritance collision.
Interfaces are resolved to classes at compiling time depending on the usage. The compiler will create an specific implementation per each usage.
interface Sortable { operator < (value Sortable); } function sort(items Array<Sortable>) { /* ... */ } function main() { let ids = [10, 54, 32]; // Array<Int> let names = ["D", "B", "X"]; // Array<String> // in order compile, `Int` and `String` // must implement the `<` operator. sort(ids); sort(names); }
function sort_Array_Int(items Array<Int>) { /* ... */ } function sort_Array_String(items Array<String>) { /* ... */ } function main() { let ids = [10, 54, 32]; let names = ["D", "B", "X"]; sort_Array_Int(ids); sort_Array_String(names); }
Lambdas are types that represents functions. When a field is declared with a lambda type, the field can be treated as a function. Any member that contain logic can be converted to a lambda value as long as it matches with the lambda type. Lambdas can also be created inline.
There are more than one way to create a lambda:
Inline lambdas are created inside instruction blocks, a closure is generated when fields are captured in the lambda:
lambda Action(); lambda Logger(message String); lambda Operation(a int, b int) int; function main() { // receives one arguments doesn't return a result let log Logger = (msg) -> { /* ... */ }; // receives two arguments a returns a result let sum Operation = (a, b) -> a + b; let div Operation = (num, den) -> { if den == 0 { throw Error.new(); } return a / b; }; // no arguments, no result let test Action = () -> { log(sum(1, 2)); // 1 + 2 = 3 log(div(3, 3)); // 3 / 3 = 1 }; test(); }
Functions can be converted to lambdas as long as they match with the type.
lambda Writer(message String); function print(message String) { /* ... */ } class Console { function log(message String) { /* ... */ } } function main() { let write1 Writer = print; let write2 Writer = Console().log; write1("test 1!"); write2("test 2!"); }
Since properties contain logic they can be converted to lambdas as well.
var name = null; getter hasName Bool { return name != null; } setter hasName(value Bool) { if value and name == null { name = "Default"; } else not value and name != null { name = null; } } lambda Getter() Bool; lambda Setter(value Bool); function test() { let get Getter = hasName; let set Setter = hasName; console.log(name); // null console.log(get()); // false set(true); console.log(get()); // true console.log(name); // "Default" }
Since fields can be converted to properties, fields can be converted to lambdas in the same way than properties.
var version = 1; lambda Getter() Int; lambda Setter(value Int); function main() { let getVersion Getter = version; let setVersion Setter = version; console.log(getVersion()); // 1 setVersion(2); console.log(getVersion()); // 2 }
Since operators contains logic, they can be converted to lambdas.
class Bag { operator `+` (bag! Bag) Bag { let merged = Bag(); // some magic to merge both bags here return merged; } } lambda Merger(bag! Bag) Bag; function main() { let bag = Bag(); let merge Merger = bag.`+`; // both lines are equivalent: let r1 = merge(Bag()); let r2 = bag + Bag(); }
Inline Lambdas can capture constant fields creating a closure.
lambda Logger(message String); function createLogger(prefix String) Logger<String> { return (message) -> { // Notice `prefix` is captured in this lambda. console.log("${prefix} - ${message}"); }; } function test() { let warn = createLogger("[W]"); let info = createLogger("[I]"); warn("Danger! Danger!"); // [W] - Danger! Danger! info("Don't talk to strangers."); // [I] - Don't talk to strangers. }
Enums are a custom type which can only have a limited set of existing values.
Enums cannot have superclass. Enums cannot be initialized.
enum Alignment { LEFT, RIGHT, CENTER, START, END, } function test() { var a Alignment = null; a = Alignment.LEFT; // OK a = Alignment("LEFT"); // OK a = Alignment(0); // OK }
Fields are variables or constants that have a value stored in the memory. All fields have a type which cannot be changed after the declaration and can be either explicitly declared or inferred through the initialization.
Fields can be only owned by following members and the lifespan will depend on the owner:
module math { let PI = 3.1415; } class Point { var x = 0; var y = 0; } function swap(list List<Any>, index1 Int, index2 Int) { let aux = list[index1]; list[index1] = list[index2]; list[index2] = aux; }
Functions are invocable blocks of code which can receive arguments and optionally produce a result.
Only when the function has a return type specifed, the function must use the return
statement
to complete the execution with a result.
Functions can be only owned by following members and the way to invoke them will depend on the owner:
function abs(n Number) Number { return n < 0 ? -n : n; } class Rectangle { var width = 0.0; var height = 0.0; constructor new(@width int, @height int) { width = @width; height = @height; } function computeArea() float { return width * height; } } function main() { // module functions can be invoked directly console.log(abs(-1)); // output: 1 // class functions need an instance to be invoked let rect = Rectangle.new(10, 20); console.log(rect.computeArea()); // output: 200 }
Functions can receive any number of parameters which are considered as fields available only during the function execution.
In order to improve readability, the keywords var
and let
are optional when declaring parameters and it is defaulted to let
.
Since parameters are passed by reference, reassignments only affects the reference in the function's body.
function test(name String, var index int!) { name = "Mat"; // ERROR: The field can't be reasigned index = index + 1; // OK! the field is explicitly `var` }
Optional parameters are allowed by specifying the initial value when declaring them. Any parameter can be optional no matter the order, however, when invoking the function sending a list of arguments, optional parameters can't be skipped. The only way to skip an optional parameter is by invoking the function using Named Arguments.
It doesn't matter if the optional parameter is let
or var
, the initial value
specified in the parameter declaration is only used when there is no value specified
in the function invocation.
function write(data List<byte>, offset int! = 0, length int = null) { // ... } function test() { let data = [1, 2, 3]; // Optional arguments received: write(data); // offset = 0, length = null write(data, 1); // offset = 1, length = null write(data, 1, 2); // offset = 1, length = 2 write(data, length: 2); // offset = 0, length = 2 }
Unlike normal fields, properties aren't allocated in memory, every time they are retrieved or assigned, the getter or setter logic is executed respectively.
Despite properties are declared separately by using getters and setters, they must comply with following rules:
When using interfaces, fields can be treated as properties transparently.
class DefaultApplication { export let version String = "1.0"; protect $baseTitle String = "Default Application"; export getter title() String { return $baseTitle + " v${version}"; } export setter title(@title String) { $baseTitle = @title; } } class CustomApplication { export var title String = "Custom Application"; protect let $majorVersion int; protect let $minorVersion int; export constructor new(@maj int, @min int) { $majorVersion = @maj; $minorVersion = @min; } export getter version() String { return $majorVersion + "." + $minorVersion; } } interface Application { let version String; var title String; } function test() { var app Application = null; app = DefaultApplication(); console.log(app.title); // "Default Application v1.0" console.log(app.version); // "1.0" app.title = "MyApp"; console.log(app.title); // "MyApp v1.0" app = CustomApplication.new(2, 3); console.log(app.title); // "Custom Application" console.log(app.version); // "2.3" app.title = "MyApp"; console.log(app.title); // "MyApp" }
TODO: Default constructors are available only when no other constructor is defined.
Constructors are the only way to create an instance of a class, every class must have at least one constructor in order to be instantiable. Constructors can only be invoked through the class name, they don't need an instance to be invoked.
When a class is inheriting from another class with already defined constructors, the constructor must be chained.
A class can be designed to be only extended and prevent it to have direct instances, this can be only reached through protected constructors. Protected constructors can only be invoked in subclasses.
Constructors are not inherited so a constructor in a subclass can have the same name of a superclass constructor.
class Entity { export let id String; protect constructor new(@id String) { id = @id; } } class User inherits Entity { export let name; // `new` constructor can be declared since they are not inherited export constructor new(@id String, @name String) : Entity.new(@id) { // super-constructor can be accessed only here name = @name; } } function test() { let entity = Entity.new("0001"); // constructor is not accessible here let user = User.new("0001", "Mathison"); // accessible constructor }
TODO: Change to operation?
Operators are special functions which can be overloaded.
Symbol | Name | Usage | Description |
---|---|---|---|
==
| Equality | this == other
| Returns a bool! indicating if this is equal to the other value.
|
>
| Greater than | this > other
| Returns a bool! indicating if this is greater than the other value.
|
<
| Less than | this < other
| Returns a bool! indicating if this is less than the other value.
|
>=
| Greater than or equal to | this >= other
| Returns a bool! indicating if this is greater than or equal to the other value.
|
<=
| Less than or equal to | this <= other
| Returns a bool! indicating if this is less than or equal to the other value.
|
[]
| Array access | result = this[id]
| Returns whatever is in the id position of this .
|
[]=
| Array assignment | this[id] = value
| Assigns the value to the id position of this .
|
class Vector { export var x = 0.0; export var y = 0.0; export var magnitude = 0.0; export var direction = 0.0; export constructor new(@x float, @y float, @m float, @d float) { x = @x; y = @y; magnitude = @m; direction = @d; } export operator + (other Vector) Vector { let result = Vector.new(); // do fancy math here return result; } } function test() { let vec1 = Vector.new(0, 0, 10, 0); let vec2 = Vector.new(0, 0, 10, -180); let sum = vec1 + vec2; Console.log(sum); // { x: 0, y: 0, magnitude: 0, direction: 0 } }
TODO
if condition1 { } else if condition2 { } else { }
switch value { case A { } case B or C { } else { } }
loop {
// do things
if condition {
break;
}
}
let stack = Stack<int>.new([10, 20, 30]); while stack.length > 0 { let item = stack.pop(); console.log("item = ${item}"); } // Output: // item = 30 // item = 20 // item = 10
for i = 0 while i < count step i++ { console.log("i = ${count}"); } // Output: // i = 0 // i = 1 // ... // i = 9
let list = [10, 20, 30]; for item in list { console.log("item = ${item}"); } else { console.log("No items found"); } // Output: // item = 10 // item = 20 // item = 30
function main() { for i = 0 while i < 100 step 1 { if i == 23 { raise RuntimeError.new("I don't like 23"); } rescue e RuntimeError { // I don't care } } }
TODO
TODO
using writer = path.createWriter(); writer.write([10, 20, 30]);
find(text: "mat", fromIndex: 10);