Elara is a multi-paradigm (although primarily functional) language with a clean syntax with little "noise".
Unlike most functional languages, Elara will not restrict the programmer for the sake of it, and prioritises developer freedom wherever possible.
(all syntax is subject to change)
Variables are declared with the syntax:
let [name] = [value]
with type inference.
For example, let age = 23
Explicit type specification:
let [name]: [Type] = [value]
For example, let name: String = "Bob"
By default, variables are reference immutable.
For mutability, the syntax let mut [name] = [value]
should be used.
This is discouraged as immutable values should always be preferred.
Functions are first class types, and are declared in a near identical way to variables:
Note that the Arrow Syntax (=>
) is used with functions to distinguish from a function call.
let printHello() => {
print("Hello World!")
}
Functions with a single expression can be declared with a more concise syntax:
let printHello => print("Hello World!")
Functions with parameters:
let printTwice(String message) => {
print(message)
print(message)
}
Functions with a clearly defined return type:
let add(Int a, Int b) => Int {
a + b
}
Elara supports a wide range of function calling syntax to try and make programming more natural and less restrictive:
printTwice("Hello")
printHello()
"Hello".printTwice()
This feature works with multiple parameters:
let addTo(Int a, Int b) => {
a + b
}
3.addTo(4)
addTo(3, 4)
the 2 calls are identical
You can also omit the parentheses and commas with infix functions (functions with 2 parameters):
3 addTo 4
Elara has collection literals for the 2 main types:
List literals are a comma separated list of elements, surrounded by square brackets.
- Empty List:
[]
- Single Element Lists:
[1]
- Multi Element Lists:
[1, 2, 3, 4]
Lists are immutable, and the recommended implementation is a persistent one to make copying more efficient.
Lists should aim to be as homogeneous as possible - that is, Lists should try to form a union of all elements' types to form the List's type.
List types are declared in the format [ElementType]
For example [Any]
, [Int]
, [() => Unit]
Map literals are a comma separated list of Entries, surrounded by curly brackets.
Entries are composed of a Key and a Value, separated by a colon. An Entry's Key and Value must both be valid expressions.
- Empty Map:
{}
- Single Element Map:
{a: "b"}
(this assumes a variable nameda
is present in the current scope) - Multi Element Map:
{
a: "b",
c: "d"
}
(Again, this assumes the presence of a
and c
)
Maps are also immutable, and are typically implemented as a hash table.
Map types follow the format {K : V}
For example: {Int : String}
, {String : () => Unit}
, {Person : Int}
Structs in Elara are Data Only They are declared with the following syntax:
struct Person {
String name
mut Int age
Int height = 110
}
And can be instantiated like so:
let mark = Person("Mark", 32, 160)
Structs can easily replicate objects with extension syntax, which is the most idiomatic way of adding functionality to structs:
struct Person {
//blah
}
extend Person {
let celebrateBirthday => {
print("Happy Birthday " + name + "!")
age += 1
}
}
from here we can do somePerson.celebrateBirthday()
as if it was a method.
The extend
syntax works with any type and can be done from any file.
The extend
syntax effectively adds inheritance too:
struct Person {
//blah
}
extend Person {
struct Student {
Topic major
}
}
This is not true "inheritance". Instead, the Student
struct will copy all the properties of Person
.
Because the type system is contract based (that is, type B
can be assigned to type A
if it has the same contract in its members),
this is effectively inheritance - we can use an instance of Student
wherever we use a Person
.
Elara features a simple, linear type system.
Any
is the root of the type hierarchy, with subtypes such as Int
, String
and Person
.
However, there are also a few quirks that aim to make the type system more flexible:
Contract based type parameters
Type parameters for generics support contract based boundary. Take for example the simple generic function, ignoring the unnecessary generic (since T can be any type):
#T
let printAndReturn(T data) => T {
print(data)
return data
}
We cannot guarantee that every type will give a user-friendly value for print
.
To work around this, we can add a boundary to T
, that only accepts types that define a toString
function:
<T { toString() => String } >
let printAndReturn(T data) => T {
print(data.toString())
return data
}
This gives programmers extra flexibility in that they can program to a specific contract, rather than a type
The namespace system in Elara is simple.
Declaring a namespace is usually done at the top of the file:
namespace elara/core
Namespaces follow the format base/module
, similar to languages like Clojure.
Importing a namespace is simple:
import elara/core
The files will now have access to the contents of all files in that namespace.
-
Lambdas are defined identically to functions:
let lambda = (Type name) => {}
Parameter types can be omitted if possible to infer from context. -
Functions are first class:
let add1 = (Int a) => a + 1
let added1List = someList map add1
- Function chaining is trivial:
someList.map(add1).filter(isEven).forEach(print)
Elara is in its very early stages, with the evaluator being nowhere near finished.
The eventual plan includes:
- Static typing.
- Compiling to native code, but also supporting other backends such as JavaScript or JVM Bytecode.
- A proper standard library.
- Allowing type inference for function parameters.